Hi there. I have a problem, which looks like a bug to me, but I'd rather post it here to make sure I don't miss anything. Below is a code, which doesn't make much sense yet, but should demonstrate the problem.
Imagine I have a function definition in a record:
Matcher : List U8 -> Result (List U8) [DoesNotMatch]
Spec : {
field_name : Str,
length : U64,
matcher : Matcher,
}
When I try to use it like this I get an error:
match_spec1 : List U8, Spec -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
match_spec1 = |string, spec|
field_value =
string
|> spec.matcher?
|> Str.from_utf8_lossy
Dict.single(spec.field_name, field_value) |> Ok
Error:
TYPE MISMATCH
Something is off with the body of the
`match_spec1` definition:
11│ match_spec1 : List U8, Spec -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
12│ match_spec1 = |string, spec|
13│ field_value =
14│> string
15│> |> spec.matcher?
This returns an `Err` of type:
[
Err [DoesNotMatch],
Ok (Dict Str Str),
]
But the type annotation on `match_spec1` says it should be:
Result (Dict Str Str) [
DoesNotMatch,
ListWasEmpty,
]a
What makes me think it's a bug is the fact, that following code, where I use a function t
of a type Matcher
, doesn't cause any type mismatches:
t : Matcher
t = |list|
when list is
[] -> Err(DoesNotMatch)
[.. as xs] -> Ok(xs)
match_spec2 : List U8, Spec -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
match_spec2 = |string, spec|
field_value =
string
|> t?
|> Str.from_utf8_lossy
Dict.single(spec.field_name, field_value) |> Ok
Could it be something wrong with Spec.matcher
being a function?
Ok, maybe I was too fast. It has nothing to do with the record, but arguments definition. The following code causes type mismatch too:
match_spec3 : List U8, Matcher -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
match_spec3 = |string, matcher|
field_value =
string
|> matcher?
|> Str.from_utf8_lossy
Dict.single("result", field_value) |> Ok
So it's rather something to do with the definition of Matcher
. Maybe I indeed miss something, so I'll be grateful for help.
Folks, sorry for spamming you and sorry I didn't spend more time on it before posting here. I think I found the problem. The problem is, Matcher
may return only one error tag: DoesNotMatch
, but the closure function match_spec
can return 2 types of errors: DoesNotMatch
and ListWasEmpty
. If I add DoesNotMatch
to the list of possible errors in Matcher
no type mismatch is raised, i.e. Matcher : List U8 -> Result (List U8) [DoesNotMatch, ListWasEmpty]
.
Still, doesn't look like expected behaviour to me.
And just to make it clear. Even if I call another function (List.first
in the example below), which could return Err ListWasEmpty
, I still get type mismatch caused by calling matcher
:
match_spec4 : List U8, Matcher -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
match_spec4 = |string, matcher|
_ = List.first(string)?
field_value =
string
|> matcher?
|> Str.from_utf8_lossy
Dict.single("result", field_value) |> Ok
Folks, sorry for spamming you and sorry I didn't spend more time on it before posting here. I think I found the problem.
All good, no problem :)
I will dig deeper on the error union issue later today.
Alright, this is the fix:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" }
import cli.Stdout
main! = |_args|
bytes = Str.to_utf8("hey")
Stdout.line!(Inspect.to_str(match_spec4(bytes, my_matcher)))
Matcher a : List U8 -> Result (List U8) [DoesNotMatch]a
my_matcher : Matcher a
my_matcher = |string|
when string is
# froms 'hey'
[104, 101, 121] -> Ok(string)
_ -> Err(DoesNotMatch)
match_spec4 : List U8, (Matcher _) -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
match_spec4 = |string, matcher|
_ = List.first(string)?
field_value =
string
|> matcher?
|> Str.from_utf8_lossy
Dict.single("result", field_value) |> Ok
Note the type var a in:
Matcher a : List U8 -> Result (List U8) [DoesNotMatch]a
And the _
in:
match_spec4 : List U8, (Matcher _) -> Result (Dict Str Str) [DoesNotMatch, ListWasEmpty]
I'll write up an explanation next.
It's useful to think of it as if the compiler uses the full error union [DoesNotMatch, ListWasEmpty]
everywhere. So if you use Result (List U8) [DoesNotMatch]
(without a type alias) the compiler patches it up later as Result (List U8) [DoesNotMatch, ListWasEmpty]
. If you use a type alias without a type variable you "lock up" the error union. If your type alias has a type variable (a
in our case) you leave it open for additional things (e.g. ListWasEmpty
).
I don't think this lines up with what is truly happening in the compiler but this may just be the simplest way to reason about it.
Feel free to ask more questions.
Thanks for detailed explanation. I think it makes sense so far. I’ll try it out later. Looks a bit hacky to be frank. Can’t be done something like [DoesNotMatch]*
instead?
Can’t be done something like
[DoesNotMatch]*
instead?
It can not, @Ayaz Hafiz can probably explain why.
Looks a bit hacky to be frank.
Uhu, I get it, I don't know why type aliases require special treatment in this case but I suspect there's no easy way around this. I will make an issue for a better error message though, that would already improve the user experience a lot.
It is best to just pretend *
doesn't exist. I think we plan to remove it actually. It means different things than people expect. It is not really a wild card.
And we could make this work if we automatically allowed for casting tags in the case it is just expansion (not sure if we plan to support this, but I know it was discussed in the past)
Oh actually, another fix may be related to open tags that are the return types of lambdas. I think that could also be made open and just work....but I'm not fully sure.
Last updated: Jul 05 2025 at 12:14 UTC