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?
that should already happen automatically
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
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")
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
huh, interesting
what happens if you do [ZeroArgsGiven]*
instead?
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
yeah that's surprising! I'm not sure why that doesn't work, but I bet @Ayaz Hafiz does :big_smile:
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]_
.
Awesome, thanks :)
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"
If you want to type that function it would be something like this:
FullState : RabbitState (TeaPartyState [])
funtime : Str -> FullState
That said, I would definitely suggest avoiding code like that in general
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
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.
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