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?
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?
can you share the whole code snippet?
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.
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))
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
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:
(especially given that the most likely conclusion would end up being that we shouldn't change how we code-generate them)
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?
So @Simon Redding 's code example would become:
Function(fn) =>
match fn(args) {
Ok(r) => Ok((r, env))
Err(err) => Err(err.widen())
}
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.
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({})
}
This also works:
main! = |_| {
color : [Red, Blue, ..]
color = Red
more_colors : [Red, Green, Blue]
more_colors = color
dbg more_colors
Ok({})
}
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({})
}
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),
].{}
Oh my bad, I should have read your messages more closely.
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({})
}
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.
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