From the tutorial:
In summary, here's a way to think about the difference between open unions in a value you have, compared to a value you're accepting:
If you have a closed union, that means it has all the tags it ever will, and can't accumulate more.
If you have an open union, that means it can accumulate more tags through conditional branches.
If you accept a closed union, that means you only have to handle the possibilities listed in the union.
If you accept an open union, that means you have to handle the possibility that it has a tag you can't know about.
Example:
» foo = Foo "Foo"
… foo
Foo "Foo" : [ Foo Str ]*
» bar : [ Bar Str ]
… bar = Bar "Bar"
… bar
Bar "Bar" : [ Bar Str ]
»
What are the practical advantages of a closed tag union value vs. an open tag union value?
Here is a function baz
that accepts foo
but not bar
(if we ignore the current compiler panic on baz foo
, documented here: https://github.com/rtfeldman/roc/issues/2344):
» bar : [ Bar Str ]
… bar = Bar "Bar"
… baz = \x ->
… when x is
… Foo y -> y
… Bar y -> y
… baz bar
── TYPE MISMATCH ───────────────────────────────────────────────────────────────
The 1st argument to baz is not what I expect:
10│ baz bar
^^^
This bar value is a:
[ Bar Str ]
But baz needs the 1st argument to be:
[ Bar a, Foo a ]
Tip: Looks like a closed tag union does not have the Foo tag.
Tip: Closed tag unions can't grow, because that might change the size
in memory. Can you use an open tag union?
»
The inability of baz
to accept bar
seems like an unnecessary limitation that would lead me to create an open tag union value instead of a closed tag union value in all situations (since a closed tag union value could only be processed by functions that process that exact tag union and nothing more). However, I'm sure that they have a purpose - in what situations might they superior to open tag union values?
Closed union allow you to be exhaustive, covering all cases in your function (for instance the Result type, you have only the Ok and Err tags). If we look at baz:
baz : [ Foo Str, Bar Str ] -> Str
baz = \x ->
when x is
Foo y -> y
Bar y -> y
Without a closed union we can't write this function because we would need to add a catchall in the when … is
and we wouldn't know want to return in that case (or a hardcoded string? not ideal).
That said I feel like [ Bar Str ]
is a strict subset of [ Bar Str, Foo Str ]
so it would probably make sense to allow it in the type system.
Yeah, closed-vs-open tag union arguments make total sense to me. Closed-vs-open tag union values, on the other hand, are confusing to me.
I see what you mean, the issue is that there is no difference between the two in the type system.
And the semantics would start to be weird I guess if there was.
Last updated: Jul 26 2025 at 12:14 UTC