Stream: beginners

Topic: Understanding closed tag unions for values


view this post on Zulip jan kili (Jan 13 2022 at 07:30):

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?

view this post on Zulip Matthias Devlamynck (Jan 13 2022 at 09:34):

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.

view this post on Zulip jan kili (Jan 13 2022 at 09:37):

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.

view this post on Zulip Matthias Devlamynck (Jan 13 2022 at 09:43):

I see what you mean, the issue is that there is no difference between the two in the type system.

view this post on Zulip Matthias Devlamynck (Jan 13 2022 at 09:45):

And the semantics would start to be weird I guess if there was.


Last updated: Jul 26 2025 at 12:14 UTC