Hi, I was trying out the zig compiler on some advent of code problems and I am really enjoying the new syntax and static dispatch.
I found the fact that closed tag unions don't merge to be surprising, take for example:
try_a : () -> Try(_, [A])
try_a = || Err(A)
try_b : () -> Try(_, [B])
try_b = || Err(B)
# This does not compile
try_both : () -> Try(_, [A, B])
try_both = || {
try_a()?
try_b()
}
If try_a and try_b produced open tag unions, try_both would compile, which is more ergonomic. Is there a reason why the same behavior is not possible for closed tag unions?
It's a lot of fun to write code in the new compiler, thank you for the awesome work!
what if you removed the type annotations from try_a and try_b?
Yes, I love the new syntax & static dispatch too. :+1: The fact that a union is closed means that it won't merge automatically, hence the behavior you observe. It's recommended to always use open unions for errors. If you really want to keep the unions closed, then you can replace try_a()? with try_a()? |_| A. There's some discussion about perhaps making this automatic.
@Anthony Bullard In that case they default to open tag unions and the code compiles
Thank you @Aurélien Geron. Open unions for errors does seem like the way to go. I was trying to understand what is the benefit of closed unions not merging, why are they different from closed unions in that way?
what is the benefit of closed unions not merging
If a closed union would merge it would not be closed.
And if we did not have closed unions, every pattern match on a tag union would need a wildcard branch _ => ...to account for the other tags.
I think I need to learn more about open and closed unions, found the fact that a branch with an open and a closed union gives a closed union particularly surprising:
closed_a : () -> [A]
closed_a = || A
open_a : () -> [A, ..]
open_a = || A
combine_a : Bool -> [A]
combine_a = |x| {
if x open_a()
else closed_a()
}
Is there documentation on how tag unions work? Found https://github.com/roc-lang/roc/blob/main/docs/langref/tag-unions.md but it isn't finished yet.
Thank you so much for the answers, if I have to wait until more documentation is available I completely understand.
Ok, I got Claude to help me and I think I understand why the behaviour that was intuitive to me doesn't make sense in the language.
In order for tags to unify I need them to be open, and I can implement a function that "opens" a union:
opens_a : [A] -> [A, ..]
opens = |x| {
match x {
A => A
}
}
But that's verbose and grows with the number of possible states.
Could there be some compiler magic to make a tag open, or do you think that would be to niche?
# Instead of having to implement opens_a and opens_b and do this:
try_both : () -> Try(_, [A, B])
try_both = || {
try_a().map_err(opens_a)?
try_b().map_err(opens_b)
}
# Could there be a `magic_open` that replaces those functions?
try_both : () -> Try(_, [A, B])
try_both = || {
try_a().map_err(magic_open)?
try_b().map_err(magic_open)
}
Could there be some compiler magic to make a tag open
Yes, we plan to implement that. We'll share more about it soon :)
Perfect! Thank you
Last updated: Aug 12 2026 at 12:35 UTC