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: Nov 09 2025 at 12:14 UTC