Stream: beginners

Topic: Type mismatch with a function in arguments


view this post on Zulip Ilya Shmygol (Apr 14 2025 at 10:26):

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?

view this post on Zulip Ilya Shmygol (Apr 14 2025 at 11:02):

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.

view this post on Zulip Ilya Shmygol (Apr 14 2025 at 11:15):

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.

view this post on Zulip Ilya Shmygol (Apr 14 2025 at 11:19):

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

view this post on Zulip Anton (Apr 14 2025 at 11:57):

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 :)

view this post on Zulip Anton (Apr 14 2025 at 12:05):

I will dig deeper on the error union issue later today.

view this post on Zulip Anton (Apr 14 2025 at 16:02):

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.

view this post on Zulip Anton (Apr 14 2025 at 16:54):

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.

view this post on Zulip Ilya Shmygol (Apr 14 2025 at 17:08):

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?

view this post on Zulip Anton (Apr 14 2025 at 17:29):

Can’t be done something like [DoesNotMatch]* instead?

It can not, @Ayaz Hafiz can probably explain why.

view this post on Zulip Anton (Apr 14 2025 at 17:33):

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.

view this post on Zulip Anton (Apr 14 2025 at 17:40):

#7742

view this post on Zulip Brendan Hansknecht (Apr 15 2025 at 03:27):

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.

view this post on Zulip Brendan Hansknecht (Apr 15 2025 at 03:28):

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)

view this post on Zulip Brendan Hansknecht (Apr 15 2025 at 03:29):

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