Stream: beginners

Topic: Error Tag Union Incompatibility


view this post on Zulip Simon Redding (Sep 25 2026 at 11:41):

Hi all,
I'm getting a bit confused about how to make error tag unions compatible.
This is the error roc check is giving me:

simon@neckbeard:~/working/roc-lisp$ roc check Interpreter.roc
── ✗ type mismatch ────────────────────────────────────────────────────────────────────────────── Interpreter.roc:228:33

This ? may return early with a type that doesn't match the function body.

Function(fn) => Ok((fn(args)?, env))
                    ^^^^^^^^^

If this Try is an Err, then the ? after it immediately returns an Err whose payload has this type:

    [EmptyList, OutOfRange, TypeMismatch, WrongArity]

Returning an Err with that type only works if the function itself returns a Try with a compatible error type, but this function's return
type is:

    Try((Value, Env), [NotAFunction(Value), WrongArity, ..])

Hint: The error types from all ? operators and the function body must be compatible, since any of them could be the actual return value.

── 1 error and 0 warnings ────────────────────────────────────────────────────────────────────────────── Interpreter.roc

Am I right in thinking that the .. means that the second tag union is open?
If so, how can the other tag union be incompatible with it?


OK... after playing around with a minimal example (attached) it seems that _both_ unions need to be open.
Unions.roc
I guess this is fair enough, but it leaves me with another problem: I can't declare a tag payload to be a function returning a Try with an open error tag union.

simon@neckbeard:~/working/roc-lisp$ roc check Interpreter.roc
── ✗ open ext not allowed in type declaration ─────────────────────────────────────────────────── Interpreter.roc:176:90

You cannot use a .. inside a type declaration.

Function(List(Value) -> Try(Value, [WrongArity, TypeMismatch, EmptyList, OutOfRange, ..])),
                                                                                     ^^

Does anyone have any ideas on how to resolve this?

view this post on Zulip Simon Redding (Sep 25 2026 at 15:14):

So I have a workaround but it feels kind of clunky.
I've had to match each return explicitly.

            Function(fn) =>
                match fn(args) {
                    Ok(r) => Ok((r, env))
                    Err(WrongArity) => Err(WrongArity)
                    Err(TypeMismatch) => Err(TypeMismatch)
                    Err(EmptyList) => Err(EmptyList)
                    Err(OutOfRange) => Err(OutOfRange)
                }

Is there a better way yo do this?

view this post on Zulip Jared Ramirez (Sep 26 2026 at 01:59):

can you share the whole code snippet?

view this post on Zulip Simon Redding (Sep 26 2026 at 08:16):

Thanks for looking at this.

simon@neckbeard:~/working/roc-lisp$ roc version
Roc compiler version nightly-2026-09-18-1d982dc

Here's the whole file:
Interpreter.roc

The crux is line ~200 where I want to declare the returned error range as open, but complier says "no", and line ~262 where I have to work around it to make the error tag unions compatible.

view this post on Zulip Richard Feldman (Sep 27 2026 at 04:28):

one relatively quick way I think we could improve this: we could make structural tag unions automatically get a method called (let's say) rewrap which would essentially do the relevant part of this match for you, and then Try could get a rewrap_ok and rewrap_err which would delegate - so then you could replace that whole match with:

Function(fn) => Ok((fn(args).rewrap_err()?, env))

view this post on Zulip Richard Feldman (Sep 27 2026 at 04:29):

there are other things we could try to do to make that method call unnecessary, but that whole subject is a big can of worms that I don't think should be in scope for 0.1.0, whereas an auto-generated method called rewrap (or some other name if we can think of one) on tag unions, plus Try.rewrap_ok and Try.rewrap_err which delegates to it, could at least make this situation nicer

view this post on Zulip Richard Feldman (Sep 27 2026 at 04:30):

at least the way we currently code-generate tag unions, the match does need to happen, and changing how we code-generate tag unions would be part of the big can of worms I don't think we should open before 0.1.0 ships :sweat_smile:

view this post on Zulip Richard Feldman (Sep 27 2026 at 04:30):

(especially given that the most likely conclusion would end up being that we shouldn't change how we code-generate them)

view this post on Zulip Aurélien Geron (Sep 27 2026 at 04:50):

I really like this, I've run into this issue a few times, and this seems like a nice and easy solution. If rewrap can only rewrap to a superset, then perhaps widen would be a good alternative name?

view this post on Zulip Aurélien Geron (Sep 27 2026 at 04:52):

So @Simon Redding 's code example would become:

            Function(fn) =>
                match fn(args) {
                    Ok(r) => Ok((r, env))
                    Err(err) => Err(err.widen())
                }

view this post on Zulip Simon Redding (Sep 27 2026 at 09:18):

These seem like reasonable solutions, but I'm curious why I can't declare the tag payload as a function returning Try with and open error union.

view this post on Zulip Aurélien Geron (Sep 27 2026 at 10:27):

I think you can, the following works fine, I just tested it:

f : U64 -> Try(U64, [TooBig, ..])
f = |x| {
    if x > 1000 { Err(TooBig) } else { Ok(x) }
}

g : U64 -> Try(U64, [TooBig, TooSmall])
g = |x| {
    if x < 100 { Err(TooSmall) } else { f(x) }
}

main! = |_| {
    res = g(2000)
    dbg res
    Ok({})
}

view this post on Zulip Aurélien Geron (Sep 27 2026 at 10:29):

This also works:

main! = |_| {
    color : [Red, Blue, ..]
    color = Red

    more_colors : [Red, Green, Blue]
    more_colors = color

    dbg more_colors
    Ok({})
}

view this post on Zulip Aurélien Geron (Sep 27 2026 at 10:32):

It also works if you make more_colors open: more_colors : [Red, Green, Blue, ..].

However, it will not work if colors is closed, regardless of whether more_colors is open or closed. This is the case where Richard's unwrap (or widen) would be useful:

main! = |_| {
    color : [Red, Blue]
    color = Red

    more_colors : [Red, Green, Blue]
    more_colors = color.widen()

    dbg more_colors
    Ok({})
}

view this post on Zulip Simon Redding (Sep 27 2026 at 10:40):

Thanks, @Aurélien Geron , but that's not quite what I'm trying to do.

I want to declare a tag that has a function as its payload where that function returns an open error union.
This code won't compile with the open union:

Env := { frames: List(Dict(Str, Value)) }.{}

Value := [
    Num(I64),
    Sym(Str),
    VList(List(Value)),
    Nil,
    Closure({ params: List(Str), body: Value, env: Env }),
    Function(List(Value) -> Try(Value, [WrongArity, TypeMismatch, EmptyList, OutOfRange, ..])),
    Boolean(Bool),
].{}

view this post on Zulip Aurélien Geron (Sep 27 2026 at 10:41):

Oh my bad, I should have read your messages more closely.

view this post on Zulip Aurélien Geron (Sep 27 2026 at 10:49):

It looks like this compiles:

Env(errs) := { frames: List(Dict(Str, Value(errs))) }.{}

Value(errs) := [
    Num(I64),
    Sym(Str),
    VList(List(Value(errs))),
    Nil,
    Closure({ params: List(Str), body: Value(errs), env: Env(errs) }),
    Function(List(Value(errs)) -> Try(Value(errs), [WrongArity, TypeMismatch, EmptyList, OutOfRange, ..errs])),
    Boolean(Bool),
].{}

main! = |_| {
    Ok({})
}

view this post on Zulip Aurélien Geron (Sep 27 2026 at 10:55):

My understanding is that you can't use open unions (..) in nominal types unless you use a type parameter for the extra errors (..errs). I'm not sure why. I'm guessing it's because eventually the compiler needs to decide on a particular list of possible tags for the union, and it needs to be the same list for all the functions in your recursive structure.

view this post on Zulip Simon Redding (Sep 27 2026 at 10:58):

Oh.... Interesting. That does make sense.
I'll see if that resolves my original issue.
Thanks for you help..


Last updated: Oct 15 2026 at 17:56 UTC