I don't really understand the table in https://www.roc-lang.org/tutorial#combining-open-unions
or maybe it is just a wording issue perhaps, or I am really not understanding
Surely [Ok Str]*
can also include Other field
, which would be rejected by [Ok Str] -> Bool
, no?
I think the second column means "can receive" Ok Str
(eg Ok "hi"
) from an [Ok Str]*
perhaps?
I think the confusion here is about what open means. Opens means that it can be merged with something else. So, if a function returns [Ok Str]*
, it would never be Other field
. It could merge with something that generates Other field
, but it couldn't possibly be returned from the original function with the return type of [Ok Str]*
.
*
in general is a bit confusing. It is so unconstrained that it can't contain any useful information.
[Ok Str]a
is a different story that probably works how you currently think [Ok Str]*
works.
Also, I think there is a bug currently. Tag unions are meant to be open by default in the return position. Sometimes I have to add an underscore to make this work correctly, so I'll write it out like this thing: Task {} [Tag1, Tag2]_
But it is not in the return position in the examples: it is being received, that is my point
Yeah, this is not related to returned tags being open
Let me try to write out some examples to explain better.
Hopefully a decent explanation of why [Ok Str]*
can be passed to [Ok Str] -> Bool
I think Jens has a point here. [Ok Str]*
can't always be passed to [Ok Str] -> Bool
. In particular, if the value of type [Ok Str]*
is a function argument, then it really could contain Other x
. Example:
acceptsClosed : [Ok Str] -> {}
acceptsClosed = \_ -> {}
acceptsOpen : [Ok Str]* -> {}
acceptsOpen = \openParameter ->
acceptsClosed openParameter # ERROR (mismatch between [...]* and [...])
returnsOpen : {} -> [Ok Str]*
returnsOpen = \{} -> Ok "foo"
passesOpen : {} -> {}
passesOpen = \{} ->
openLocalVariable = returnsOpen {}
acceptsClosed openLocalVariable # no error...
In fact, if I add a type annotation openLocalVariable : [Ok Str]*
, then both examples error
passesOpenAnnotated : {} -> {}
passesOpenAnnotated = \{} ->
openLocalVariable : [Ok Str]*
openLocalVariable = returnsOpen {}
acceptsClosed openLocalVariable # ERROR (mismatch between [...]* and [...])
I think it might be helpful to distinguish between [Ok Str]*
and [Ok Str]_
here. If I write openLocalVariable : [Ok Str]_
then there's no error, because _
can be chosen to be []
, and [Ok Str][]
is [Ok Str]
, which makes the whole thing type correct.
It would be an accurate statement to say "If I have a local of type [Ok Str]_
, it can be passed to a function of type [Ok Str] -> Bool
" But I don't think the original statement is accurate
It seems that in general in Roc, when you're defining a top-level function, *
represents a placeholder instantiated by each of your callers, whereas _
represents a placeholder which you get to instantiate.
In fact, if I add a type annotation
openLocalVariable : [Ok Str]*
, then both examples error
Yeah, I'm not sure if that is a bug or not. It definitely is confusing. Realized that early and started #compiler development > Is this a bug?.
"If I have a local of type
[Ok Str]_
, it can be passed to a function of type[Ok Str] -> Bool
"
Need to add a caveat that the _
needs to not merge with anything else for that to work.
Eg
generateOpenOkStr : Str -> [Ok Str]*
generateOpenOkStr = \s ->
Ok s
restrictsToClosed : [Ok Str] -> Bool
restrictsToClosed = \Ok s->
!(Str.isEmpty s)
restrictsToDifferentClosed : [Ok Str, Other] -> Bool
restrictsToDifferentClosed = \union->
when union is
Ok s -> !(Str.isEmpty s)
Other -> Bool.false
expect
in : [Ok Str]_
in = generateOpenOkStr ""
out0 = restrictsToDifferentClosed in
out1 = restrictsToClosed in # Type error: `[Other, …]` vs `[…]`
out0 == out1
Last updated: Jul 06 2025 at 12:14 UTC