Stream: beginners

Topic: Confusion about tasks


view this post on Zulip Mythmon (Jun 24 2024 at 00:25):

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?

view this post on Zulip Luke Boswell (Jun 24 2024 at 00:46):

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.

view this post on Zulip Luke Boswell (Jun 24 2024 at 00:47):

So I'm guessing to make the second work you need to add wrap the final expression in a Task.ok

view this post on Zulip Luke Boswell (Jun 24 2024 at 00:48):

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) _.

view this post on Zulip Kilian Vounckx (Jun 24 2024 at 04:30):

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

view this post on Zulip Mythmon (Jun 24 2024 at 05:07):

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 to await in other languages. It means “wait until the asynchronous File.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.

view this post on Zulip Brendan Hansknecht (Jun 24 2024 at 05:23):

thats a really good point. In roc, the task is part of the type system and is a contract.

view this post on Zulip Brendan Hansknecht (Jun 24 2024 at 05:23):

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.

view this post on Zulip Brendan Hansknecht (Jun 24 2024 at 05:23):

That is structurally equivalent to returning a task in roc.

view this post on Zulip Brendan Hansknecht (Jun 24 2024 at 05:24):

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

view this post on Zulip Sam Mohr (Jun 24 2024 at 05:31):

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

view this post on Zulip Akeshihiro (Jun 24 2024 at 05:35):

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.

view this post on Zulip Brendan Hansknecht (Jun 24 2024 at 05:57):

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.

view this post on Zulip Sam Mohr (Jun 24 2024 at 05:58):

That would be viable, then, yep

view this post on Zulip Kilian Vounckx (Jun 24 2024 at 08:25):

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

view this post on Zulip Sam Mohr (Jun 24 2024 at 08:40):

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 Taskthat calls Stdout.line! ... at the end will ignore that !), so we've already lost our "perfect purity".

view this post on Zulip Kilian Vounckx (Jun 24 2024 at 08:51):

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 returns Taskthat calls Stdout.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.

view this post on Zulip Kilian Vounckx (Jun 24 2024 at 08:52):

But I guess we agree we shouldn't autowrap

view this post on Zulip Brendan Hansknecht (Jun 24 2024 at 16:03):

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