Stream: beginners

Topic: tag union combinations


view this post on Zulip Anton (Jun 06 2023 at 15:07):

Task.await is currently defined as Task a err, (a -> Task b err) -> Task b err, I'd like to have Task a erra, (a -> Task b errb) -> Task b errc where errc is the combination of the tag unions of erra and errb, is that possible?

view this post on Zulip Richard Feldman (Jun 06 2023 at 15:17):

that should already happen automatically

view this post on Zulip Richard Feldman (Jun 06 2023 at 15:18):

in the sense that if you give it two tasks that each have tag unions for their error types, they should all unify and the returned task should have the union of their tags

view this post on Zulip Anton (Jun 06 2023 at 16:09):

That does work when I let the compiler infers the types but not with my own "narrow" type annotation, see comments above readFirstArgT and readFileToStr:

app "command-line-args"
    packages {
        pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.2/tE4xS_zLdmmxmHwHih9kHWQ7fsXtJr7W7h3425-eZFk.tar.br",
    }
    imports [
        pf.Stdout,
        pf.Stderr,
        pf.File,
        pf.Path,
        pf.Task,
        pf.Arg,
    ]
    provides [main] to pf

main : Task.Task {} []
main =
    finalTask =
        # try to read the first command line argument
        pathArg <- Task.await readFirstArgT

        readFileToStr (Path.fromStr pathArg)

    finalResult <- Task.attempt finalTask

    when finalResult is
        Err ZeroArgsGiven ->
            Stderr.line "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc command-line-args.roc -- path/to/input.txt`"

        Err (ReadFileErr errMsg) ->
            Stderr.line "Error ReadFileErr:\n\t\(errMsg)"

        Ok fileContentStr ->
            Stdout.line "Success! \n\t\(fileContentStr)"

# Task to read the first CLI arg (= Str)
# readFirstArgT : Task.Task Str [ZeroArgsGiven]
readFirstArgT =
    # read all command line arguments
    args <- Arg.list |> Task.await

    # get the second argument, the first is the executable's path
    List.get args 1 |> Result.mapErr (\_ -> ZeroArgsGiven) |> Task.fromResult

# reads a file and puts all lines in one Str
# readFileToStr : Path.Path -> Task.Task Str [ReadFileErr Str]
readFileToStr = \path ->
    path
    |> File.readUtf8
    # Make a nice error message
    |> Task.mapFail
        (\_ -> ReadFileErr "failed to read file")

view this post on Zulip Anton (Jun 06 2023 at 16:11):

I can turn readFirstArgT : Task.Task Str [ZeroArgsGiven] into readFirstArgT : Task.Task Str [ZeroArgsGiven, ReadFileErr Str]but that feels dirty, because it's overly broad :p

view this post on Zulip Richard Feldman (Jun 06 2023 at 16:20):

huh, interesting

view this post on Zulip Richard Feldman (Jun 06 2023 at 16:21):

what happens if you do [ZeroArgsGiven]* instead?

view this post on Zulip Anton (Jun 06 2023 at 16:22):

Yeah, I tried that as well:

❯ ./roc_nightly/roc examples/CommandLineArgs/main.roc -- examples/CommandLineArgs/input.txt

── TYPE MISMATCH ─────────────────────────── examples/CommandLineArgs/main.roc ─

This 2nd argument to await has an unexpected type:

20│>          pathArg <- Task.await readFirstArgT
21│>
22│>          readFileToStr (Path.fromStr pathArg)

The argument is an anonymous function of type:

    Str -> Task Str [ReadFileErr Str]

But await needs its 2nd argument to be:

    Str -> Task Str [ZeroArgsGiven]*

Tip: Seems like a tag typo. Maybe ReadFileErr should be ZeroArgsGiven?

Tip: Can more type annotations be added? Type annotations always help
me give more specific messages, and I think they could help a lot in
this case


── TYPE MISMATCH ─────────────────────────── examples/CommandLineArgs/main.roc ─

This 2nd argument to attempt has an unexpected type:

24│>      finalResult <- Task.attempt finalTask
25│>
26│>      when finalResult is
27│>          Err ZeroArgsGiven ->
28│>              Stderr.line "Error ZeroArgsGiven:\n\tI expected one argument, but I got none.\n\tRun the app like this: `roc command-line-args.roc -- path/to/input.txt`"
29│>
30│>          Err (ReadFileErr errMsg) ->
31│>              Stderr.line "Error ReadFileErr:\n\t\(errMsg)"
32│>
33│>          Ok fileContentStr ->
34│>              Stdout.line "Success! \n\t\(fileContentStr)"

The argument is an anonymous function of type:

    [
        Err [
            ReadFileErr Str,
            ZeroArgsGiven,
        ],
        Ok Str,
    ] -> Task {} *

But attempt needs its 2nd argument to be:

    Result Str [ZeroArgsGiven]* -> Task {} *

────────────────────────────────────────────────────────────────────────────────

2 errors and 1 warning found in 42 ms.

You can run the program anyway with roc run examples/CommandLineArgs/main.roc

view this post on Zulip Richard Feldman (Jun 06 2023 at 16:24):

yeah that's surprising! I'm not sure why that doesn't work, but I bet @Ayaz Hafiz does :big_smile:

view this post on Zulip Ayaz Hafiz (Jun 06 2023 at 16:32):

it's because Task.Task is an opaque type, and not all tag unions can be open-by-default-in-output position under an opaque type (but type parameters like the tags here should be). I'll file an issue, but in the meantime you can work around this by saying [ZeroArgsGiven]_ and [ReadFileErr Str]_.

view this post on Zulip Anton (Jun 06 2023 at 16:46):

Awesome, thanks :)

view this post on Zulip David Dunn (Jan 20 2024 at 09:03):

I'm having some trouble understanding how to type named tag unions when I want to combine those unions. What is the inferred type of funtime in the example below? If it is just the union [Late, OnTime, Ongoing, Stopped], is there a way to express that type in terms of RabbitState a and TeaPartyState a?

app "hello"
    packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.1/Icc3xJoIixF3hCcfXrDwLCu4wQHtNdPyoJkEbkgIElA.tar.br" }
    imports [pf.Stdout]
    provides [main] to pf

RabbitState a : [OnTime, Late]a

rabbit : Str -> RabbitState a
rabbit = \str ->
    if str == "lateforaveryimportantdate" then
        Late
    else
        OnTime

TeaPartyState a : [Ongoing, Stopped]a

teaParty : Str -> TeaPartyState a
teaParty = \str ->
    if str == "time said enough" then
        Stopped
    else
        Ongoing

# fails because when produces [Late, OnTime, Ongoing, Stopped]
# funtime : Str -> [RabbitState a, TeaPartyState a]
funtime = \str ->
    # when fails because it produces [Late, OnTime, Ongoing, Stopped]
    when Str.countGraphemes str is
        x if x > 16 -> rabbit str
        _ -> teaParty str

    # # if fails because rabbit branch produces [Late, OnTime]
    # if Str.countGraphemes str > 16 then
    #     rabbit str
    # else
    #     teaParty str

main =
    dbg (funtime "beep")
    Stdout.line "hello world"

view this post on Zulip Brendan Hansknecht (Jan 20 2024 at 17:38):

If you want to type that function it would be something like this:

FullState : RabbitState (TeaPartyState [])
funtime : Str -> FullState

view this post on Zulip Brendan Hansknecht (Jan 20 2024 at 17:39):

That said, I would definitely suggest avoiding code like that in general

view this post on Zulip Brendan Hansknecht (Jan 20 2024 at 17:41):

I think tagged wrapping or just defining the final output type makes a lot more sense in almost all cases:

funtime : Str -> [Rabbit RabbitState, TeaParty TeaPartyState] # note, need to remove the type variable from each for this
funtime = \str ->
    when Str.countGraphemes str is
        x if x > 16 -> rabbit str |> Rabbit
        _ -> teaParty str |> TeaParty

view this post on Zulip David Dunn (Jan 24 2024 at 09:29):

Thanks! Agreed, combining tags like that makes it harder to track the meaning of each tag. Also, if the types RabbitState and TeaPartyState ended up sharing a tag like Late, the caller wouldn't know the context of the Late tag - it could mean something different for each state type.

view this post on Zulip David Dunn (Jan 24 2024 at 09:30):

I was doing this more to push the bounds of open tag unions so I can understand them better. Thanks for the clear response!


Last updated: Jul 06 2025 at 12:14 UTC