So, I'm not fully sure what is going wrong, but I feel like my mental model for combining tags isn't working. Currently, my sqlite PR isn't working due to breaking during compilation and it seems to be due to tag combining. Fundamentally, I have some errors that are innate to interacting with sql in various ways and some that would be user defined. I am trying to use them together in a flat union instead of adding extra unnecessary layers.
So for the sqlite result decoder, we have the base error:
SqlDecodeErr err : [FieldNotFound Str, SqlError Code Str]err
SqlDecode a err := List Str -> (Stmt -> Task a (SqlDecodeErr err))
Then it is used with various other error types and attempted to be merged together.
For example, if you query for exactly 1 result, it adds two more possible errors:
RowCountErr err : [NoRowsReturned, TooManyRowsReturned]err
So then in the full function call, we add some layers of nesting to build of the full tag as SqlDecodeErr (RowCountErr err)
.
This leads to the function type I would want to write:
decodeExactlyOneRow : Stmt, SqlDecode a err -> Task a (SqlDecodeErr (RowCountErr err))
Sadly, this fails to check. Cause the input err
type does not know about the RowCountErr
s. Roc will not expand the tag. So it is a type mismatch with unexpected types:
The argument is an anonymous function of type:
a -> Task a [
SqlError InternalSql.SqliteErrCode …,
TooManyRowsReturned,
]
But this function needs its 2nd argument to be:
a -> Task a [SqlError Code …, …]err
So I have to type the input error type the same as the output error type to force roc to allow them to unify happily:
decodeExactlyOneRow : Stmt, SqlDecode a (RowCountErr err) -> Task a (SqlDecodeErr (RowCountErr err))
This now passes roc check, but fails to compile:
thread '<unnamed>' panicked at crates/compiler/mono/src/ir.rs:6124:9:
assertion failed: unspecialized.is_empty()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
My general questions are:
Task
usually does where all of the various error types just merge.Have you tried making the error tag union here an open union?
a -> Task a [
SqlError InternalSql.SqliteErrCode …,
TooManyRowsReturned,
]*
I don't think it should be necessary, but that could be a type bug
I'm not sure how to make it open given it is generated from merging tags.
It is a SqlDecodeErr (RowCountErr err)
If I substitute types, that is a [FieldNotFound Str, SqlError Code Str]([NoRowsReturned, TooManyRowsReturned]err)
I think
Brendan Hansknecht said:
This leads to the function type I would want to write:
decodeExactlyOneRow : Stmt, SqlDecode a err -> Task a (SqlDecodeErr (RowCountErr err))
Sadly, this fails to check. [...] So I have to type the input error type the same as the output error type to force roc to allow them to unify happily:
decodeExactlyOneRow : Stmt, SqlDecode a (RowCountErr err) -> Task a (SqlDecodeErr (RowCountErr err))
I think the compiler is correct here; in general, a type alias means essentially "inline this type wherever you see the alias," and looking at the type signatures after inlining reveals the problem:
Stmt, SqlDecode a err -> Task a (SqlDecodeErr (RowCountErr err))
# after inlining the type aliases:
Stmt, SqlDecode a err -> Task a [FieldNotFound Str, SqlError Code Str][NoRowsReturned, TooManyRowsReturned]err
so after inlining, we can see that in SqlDecode a err
the err
is referring to "all the other tags in the error union except [FieldNotFound Str, SqlError Code Str][NoRowsReturned, TooManyRowsReturned]
" because that's what [...]err
means.
Here's inlining the other one:
Stmt, SqlDecode a (RowCountErr err) -> Task a (SqlDecodeErr (RowCountErr err))
# after inlining the type aliases:
Stmt, SqlDecode a [NoRowsReturned, TooManyRowsReturned]err -> Task a [FieldNotFound Str, SqlError Code Str][NoRowsReturned, TooManyRowsReturned]err
I'm a little surprised that one type-checks; I'd expect it to have to be:
Stmt, SqlDecode a (SqlDecodeErr (RowCountErr err)) -> Task a (SqlDecodeErr (RowCountErr err))
@Folkert de Vries and @Ayaz Hafiz probably have a more accurate mental model than I do of how these are unifying though :big_smile:
The second one checks. SqlDecode
adds a SqlDecodeErr
internally
That said, even though it type checks it crashes the compiler.
I think this is a bug
pretty sure the first version should work
are type variable for tags open or closed? Is there anyway to control that?
Also, Richard, I think the original type should work cause SqlDecode a err
is a lambda with the final lambda doing: (Stmt -> Task a (SqlDecodeErr err))
. Since output tags are supposed to be open. This should return Task with an open tag output.
Oh, does the open tag in return types work if the tag is nested in another type. Like I32 -> [Even, Odd]
will return an open tag. Will I32 -> Task [Even, Odd] ...
return an open tag?
A little update on this. The crash is due to an error with an empty tag in it. Apparently roc requires that to be typed or crash:
Builder func:
intDecoder : (I64 -> Result a err) -> (Str -> SqlDecode a [FailedToDecodeInteger err]UnexpectedTypeErr)
intDecoder = \cast ->
decoder \val ->
when val is
Integer i -> cast i |> Result.mapErr FailedToDecodeInteger
_ -> toUnexpectedTypeErr val
I64 case. This crashes if the type isn't specified. I think it is due to []
.
i64 : Str -> SqlDecode I64 [FailedToDecodeInteger []]UnexpectedTypeErr
i64 = intDecoder Ok
I guess I am calling mapErr
on a Result I64 []
. Not sure what that is expected to do.
Last updated: Jul 06 2025 at 12:14 UTC