Stream: ideas

Topic: chaining but with `Task.attempt` and `?` for error returns


view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 21:56):

Chaining got me thinking. Maybe there is a better way to express the flow and error cases.

My main issue with the default implementation given for ! as |> Task.await is that you lose errors.
All errors essentially are treated as exceptions that are pushed off for the wrapping function to deal with.

Personally, I don't like this implicit loss of local error control.

I think we might create a nicer api overall by keeping the results around and instead adding
functionality to pass errors up the stack in an early return sort of a way.

I would prefer to be able to do this:

# No error case, just get x directly
x = Stdin.line!

Stdout.line! x

# This has an error case we get the full result.
res = File.readBytes! path

# We don't want to handle the error locally though
bytes = res?

In the above example, ! was chosen to map to Task.attempt and will always bring the error case locally.
? was chosen as an explicit unwrap mechanism.
It will lead to any errors early returning such that the local situation can ignore the error.
I guess if really wanted, we could use !? to merge the two behaviours together in one location.

Note: ! and ? could be symbols or keywords. I am trying to avoid the exact syntactical assessment and focus more on the differences from #ideas > chaining syntax in terms of error behavior and propagation.

view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 21:59):

A quick not on how this applies to other interafaces.

For result, you would never need !, you would just use ? to send the error.

For things link an rng or other various implementations, we could theoretically expand ! to still use with and support them. They would just always succeed without an error case and would not need to be used with ?

view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 22:01):

Also, before someone asks what all the types are to make this work, they don't work. ! is technically two different functions depending on if a task can return an error or not. That is for user convenience and I think would be worth adding compiler magic. Otherwise, there would be many ? for types that don't even have an error case. It is just clutter.

view this post on Zulip Eli Dowling (Jan 08 2024 at 22:18):

I really like this idea.
Particularly the ! ? !? combo, I think it's super natural and intuitive
I was a little confused by your mention of Task.attempt and why that's even necessary and I realized I actually don't really understand why tasks that can fail don't return a result, could you point me to an explanation of that?

view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 22:23):

The core difference is in how Task.attempt and Task.await handle the error case.

Task.await only cares about the happy path. Essentially, if everything is good, continue like so.
As such, all possible error cases will be aggregated together into one gigantic error. That entire gigantic error is what will be returned and left for the calling function to deal with.

Task.attempt on the other hand deals with both paths together. It gives a result that contains the happy path and the potential errors.

Talking in terms of the syntax shown above, Task.attempt is !. Task.await is !?.

view this post on Zulip Richard Feldman (Jan 08 2024 at 23:41):

Brendan Hansknecht said:

Also, before someone asks what all the types are to make this work, they don't work. ! is technically two different functions depending on if a task can return an error or not. That is for user convenience and I think would be worth adding compiler magic.

I don't think this compiler magic works without changing Roc's type system somehow; otherwise, what happens if I put this into the repl?

» \arg ->
    x = arg!
    x

in the #ideas > chaining syntax proposal, the repl would print:

<function> : Task a b -> Task a b

what would it print with this compiler magic?

view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 23:49):

I mean if we can't do the magic, just means that it has to be !? even if a task has no possible errors.

I guess that's the cost for wrapping everything in task even when It could just be a raw effect

view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 23:52):

Though maybe there is a nicer API we could make just on effect without the task wrapping.

view this post on Zulip Brendan Hansknecht (Jan 08 2024 at 23:52):

Not really sure. Would definitely need to mess with types.


Last updated: Jun 16 2026 at 16:19 UTC