Spinoff from #beginners > combining open unions, is the following a bug?
interface Test
exposes []
imports []
generateOpenOkStr : Str -> [Ok Str]*
generateOpenOkStr = \s ->
Ok s
restrictsToClosed : [Ok Str] -> Bool
restrictsToClosed = \Ok s->
!(Str.isEmpty s)
expect
# `in` is an open tag union, returned as `[Ok Str]*`.
# Yet, if I type it as such, it breaks due to type mismatch
in : [Ok Str]*
in = generateOpenOkStr ""
out = restrictsToClosed in
out == Bool.false
This 1st argument to restrictsToClosed has an unexpected type:
18│ out = restrictsToClosed in
^^
This in value is a:
[…]*
But restrictsToClosed needs its 1st argument to be:
[…]
Works if in : [Ok Str]*
is commented out.
It does work with in : [Ok Str]_
I think it's that bug, I'll check if we have an issue for that.
No, #5660 is different, perhaps this is intended behavior, given that the error message states:
(The * means something different when the tag union is an argument to a
function, though!)
Yeah, it may be a proper error. That said, it definitely feels wrong as a proper error given I am returning a [Ok Str]*
from the function.
Am I storing the return type [Ok Str]*
or the argument type [Ok Str]*
? Very unclear.
Whatever the case, I definitely think we need a clearer explanation here at a minimum.
I think this is a reasonable way for *
and _
to work. I can reproduce this behavior with List *
and List _
and I think it makes sense
constructList : List _
constructList =
empty : List _
empty = []
list1 : List _
list1 = List.append empty 1 # no error
list1
constructList2 : List *
constructList2 =
empty : List *
empty = []
list1 : List *
list1 = List.append empty 1 # ERROR (mismatch between Num * and *)
list1
The mental model here is that within a top-level declaration, *
is a placeholder instantiated by each caller and _
is a placeholder which we get to instantiate
Any time you write myLocal = myTopLevel
where myTopLevel : MyType *
, you get myLocal : MyType _
. This is exactly what it means that *
is chosen by each caller; we are a caller of myTopLevel
, so now we get to choose how to instantiate the *
I agree that without function calls, that all makes sense. I think it feels the worst when returning something from a function:
getEmpty: {} -> List *
getEmpty = \{} -> []
# I know why this doesn't work, but it feels like it should work.
# `empty` is simply specifying the exact same type as what was returned by `getEmpty`
constructList : List _
constructList =
empty : List *
empty = getEmpty {}
list1 = List.append empty 1 # ERROR (mismatch between Num * and *)
list1
I wonder if it would help to have some sort of explicit input type vs return type *
....not sure, but this definitely is confusing even if it can be logically explained.
But constructList
can't possibly have type List *
there, right? The list it returns always include a number
Sorry, meant List _
or List (Num *)
there.
What if we changed the meaning of *
inside of type annotations for locals to mean the same thing as _
?
I don't think that would result in any new type errors, because we can always assign _
to *
if needed. e.g. the following typechecks
takesAnyList : List * -> {}
takesAnyList = \anyList ->
underscoreList : List _
underscoreList = anyList # no error
{}
But it has the downside of losing some precision in what type annotation are expressible. You could no longer assert that a local has a type which is indeed chosen by the caller.
Ooh there is some interesting non-uniformity with how local function definitions behave here
myNested : List U32
myNested =
myEmpty : {} -> List *
myEmpty = \{} -> []
List.append (myEmpty {}) 1 # no error, but if we change to `myEmpty : List *` we get an error like before
oh actually this non-uniformity is observable even without type annotations:
nestedPolymorphism : (List U32, List Str)
nestedPolymorphism =
# myEmpty : {} -> List *
myEmpty = \{} -> []
(List.append (myEmpty {}) 1, List.append (myEmpty {}) "1")
failedPolymorhpism : (List U32, List Str)
failedPolymorhpism =
# myEmpty : List _
myEmpty = []
(List.append myEmpty 1, List.append myEmpty "1") # ERROR (mismatch between Str and Num *)
I guess local function definitions can be polymorphic but local values cannot
Yeah, requiring functions for polymorphism was something @Ayaz Hafiz did for complexity and compile time reasons.
Top levels are special to allow polymorphism. They are always turned into functions.
Oh wait, it seems like *
in local annotations is currently completely useless, because it doesn't even specify which caller-chosen variable it's referring to
twoEmptyLists : (List *, List *)
twoEmptyLists =
empty1 : List * # The two `*` in the signature are distinct variables. Which one are we referring to?
empty1 = []
empty2 : List *
empty2 = []
(empty1, empty2) # ERROR (mismatch between (List *, List *) and (List *, List *))
In fact, you get the same error (albeit with a better message) even if there's only one *
in the signature
myEmpty : List *
myEmpty =
empty : List *
empty = []
empty # ERROR
I think the only way to use *
in a type annotation for a local successfully is if the enclosing function has _
s in its type
myEmpty2 : List _
myEmpty2 =
empty : List *
empty = []
empty # no error
Yeah, so maybe the error should say "Putting *
in a type definition is not really useful. Consider using _
instead." The only cases where *
is really useful is for function inputs.
That may not be a totally fair statement, but I do think it is mostly accurate
Brendan Hansknecht said:
Top levels are special to allow polymorphism. They are always turned into functions.
Is this accurate? I just read Let-generalization: Let’s not? and it seems to specify that only lambdas and numerals can be polymorphic, regardless of whether they're top-level or local. Isn't the fact that top-levels constants can't always be polymorphic exactly why we have Dict.empty {}
instead of just Dict.empty
?
I think the bug tracked at https://github.com/roc-lang/roc/issues/5536 is the same as the bug in this thread. In that example it's actually a top-level definition being annotated!
I thought we made all top levels into thunks to help with this
But I don't know all the details
only values that are syntactic functions or numbers can be polymorphic, regardless of whether they're on the toplevel or not
And in nested contexts, only syntactic functions can be polymorphic
To the original post, I think the program should fail to compile, but the bug is that you should not be able to type in : [Ok Str]*
. That type doesn't make any sense in Roc, since it claims that in
is polymorphic, but it's not.
Thanks for the clarity
ok cool, so it sounds like the fix here is to make a new compiler error for annotations of this form, yeah?
yeah I think so. I'm kind of surprised such a check isn't already around
I want to make an issue for this, can someone summarize in which cases an annotation containing ]*
needs to error?
If an annotation contains *
or introduces a new type variable, the associated definition must be either a syntactic function or a number. So e.g.
# OK. It's a number
three : Int *
three = 3
# OK. It's a function
returnEmpty : {} -> List *
returnEmpty \{} -> []
# Error. It's a list literal
empty : List *
empty = []
# Error. It's a tag
red : [Red, Green, Blue]*
red = Red
# Error. It's a division operation
pi : Frac *
pi = 22 / 7
Let-generalization: Let’s not? has an error message we could use as a starting point for all these cases:
-- NUMBER IS NOT POLYMORPHIC -- The type annotation on `piApprox` suggests that it can be used polymorphically: 1 | piApprox : Frac * 2 | piApprox = 22 / 7 Unfortunately, I can't use `piApprox` as any fractional type! I can only use it as exactly one of `Dec`, `F32`, or `F64`. If you want me to infer the fractional type that should be used, you can use an inference annotation instead of `*`: piApprox : Frac _ If you explicitly want `piApprox` to be able to be used polymorphically, consider making it a thunk: piApprox : {} -> Frac * piApprox = \{} -> 22 / 7
Lovely thanks @timotree! I made #6663
Last updated: Jul 06 2025 at 12:14 UTC