I'm struggling quite a bit with understanding tasks, even with the examples on the site (thanks for the examples!). I've been puzzling through it, and eventually finding some solutions, but I've hit a point that seems worth asking about.
I would have expected these two functions to all be equivalent, but they aren't.
loadCharacter1 : Path.Path -> Task.Task (List Str) _
loadCharacter1 = \path ->
File.readUtf8 path
|> Task.map \contents -> Parser.strToSteps contents
loadCharacter2 : Path.Path -> Task.Task (List Str) _
loadCharacter2 = \path ->
contents = File.readUtf8! path
Parser.strToSteps contents
loadCharacter1
works, but loadCharacter2
gives me an error I don't understand:
This 2nd argument to this function has an unexpected type:
53│ contents = File.readUtf8! path
^^^^^^^^^^^^^^^^^^^
The argument is an anonymous function of type:
Str -> List Str
But this function needs its 2nd argument to be:
Str -> InternalTask.Task c [
FileReadErr Path.Path InternalFile.ReadErr,
FileReadUtf8Err Path.Path [BadUtf8 Utf8ByteProblem U64],
]
Can someone help me understand what this error is saying, and how I should be using tasks here?
Im not sure what Parser.strToSteps
is. But the bit you are missing is that the !
bang operator is equivalent to Task.await
which is different from Task.map
.
So I'm guessing to make the second work you need to add wrap the final expression in a Task.ok
Oh, actually we can see that from the error message. So yeah, the first is mapping the success value in the task, while the second is currently trying to return a List Str, but it should be returning a Task (List Str) _.
I think this is one of the places where error messaging could be improved. With the bang syntax, it is not really clear that it desugars to await for a beginner. So having the error message refer to 'an anonymous function' can be quite confusing for a beginner I would think
Maybe having some specific language in the error around it could help, but I think my problem is more conceptual. The Roc home page says about !
:
The
!
operator is similar toawait
in other languages. It means “wait until the asynchronousFile.readUtf8
operation successfully completes.”
and the tutorial says
Basically, [Task.await] creates a task which runs one task, and then runs a second task which can use the output of the first task. (If the first task fails, the second task never gets run.)
With "await
in other languages" it doesn't matter what comes after the line that uses await, it just continues normal execution. It isn't clear that !
requires the rest of the function to already be a Task, nor is it clear to me what that even means. I've puzzled through a few more functions like this now, and I still don't think I know if I can use !
or not unless I just try it.
thats a really good point. In roc, the task is part of the type system and is a contract.
That said, this is true of other languages as well, but in a less direct way. They normally include async
as part of the function definition.
That is structurally equivalent to returning a task in roc.
Though in roc, you have to be explicit. It would probably be helpful to automatically wrap the final statement in roc for function that return tasks. This would make it feel work like other languages where you just specify a function is async
Brendan Hansknecht said:
Though in roc, you have to be explicit. It would probably be helpful to automatically wrap the final statement in roc for function that return tasks. This would make it feel work like other languages where you just specify a function is
async
This sounds nice, but then it kinda breaks Roc's feature that functions work the same with or without their type annotations. It sounds like what you're describing would return a Task with a type annotation, but a Result or whatever without
Well, I don't think that auto wrapping is a good idea because at least to me this is something I hate abot the async await feature in C# (my current language in my day job). I guess that this was made for convenience, but as I saw this at first (the method's return type is Task<string>
but the method returns a string) I was confused why the method returns the inner type of the task instead of the task. Yes, such automagic sugar reduces some clutter, but on the other hand it can lead to confusion when there is some sort of a regular convention (like methods return a value of the same type as the method's return type) and then there are exceptions to that rule which you have to learn.
To clarify, I think this would only happen with functions that use !
. Basically, the !
would be the indicator that autowrapping is safe. Not saying it is a great idea, but it would be deterministic.
That would be viable, then, yep
Brendan Hansknecht said:
To clarify, I think this would only happen with functions that use
!
. Basically, the!
would be the indicator that autowrapping is safe. Not saying it is a great idea, but it would be deterministic.
I don't think it is a good idea. I know this is rare and probably a code smell, but at the moment, having a function return Task (Task _ _) _
is totally legal. If you always autowrap the last expression with Task.ok
, you couldn't explicitly wrap it. Because if you explicitly wrapped it, it would become such a nested Task, which is probably not what the user wants
You could explicitly wrap it by annotating the return type to be Task (Task _)
, but it wouldn't be great to have to do that. However, we do already automatically drop !
usages at the end of a function if they're for the return value (e.g. a function that returns Task
that calls Stdout.line! ...
at the end will ignore that !
), so we've already lost our "perfect purity".
Sam Mohr said:
we do already automatically drop
!
usages at the end of a function if they're for the return value (e.g. a function that returnsTask
that callsStdout.line! ...
at the end will ignore that!
), so we've already lost our "perfect purity".
But this doesn't mean the type can't be inferred. You could say that !
at the end desugars to Task.await _ \x -> Task.ok x
. It wouldn't make sense to wrap it. Autowrapping however would either need explicit type annotation, or the weird corner case of nested Tasks.
But I guess we agree we shouldn't autowrap
I think autowrapping could be nice, but a relatively unimportant feature in the scheme of things.
I think the real essence of the comment above is that auto-wrapping probably doesn't have a clear integration with type inference. Without type inference, it is trivial. Optionally add a Task.ok
if that yields the type requested by the output.
With type inference, I don't think there is a flexible rule to decide when to wrap. I think you are stuck with a strict rule. I don't think a strict rule is the right choice. You don't really want always Task.ok
or never Task.ok
. You want an extra Task.ok
when it is convenient.
As a related note, this is essentially the same as having implicit type conversion/implicit type constructors (just in a really narrow scope). Which roc doesn't have anywhere else....well, kinda a little bit with tag merging, but that is different.
Last updated: Jul 06 2025 at 12:14 UTC