Stream: beginners

Topic: Understanding tasks


view this post on Zulip Luke Boswell (Mar 13 2024 at 01:31):

I've been thinking about how best to help newcomers with understanding Task and effects. Specifically how with roc we are building up a data structure which describes what to do, and then passing that off to the platform to actually do the thing... so chaining tasks doesn't actually do anything until they are executed.

It feels to me like this is the most important concept to work effectively with roc.

I like a good graph or illustration.

I've been thinking if it would be possible to take a program written for e.g. basic cli, swap out the platform URL, and it generates some kind of graph which shows the task sequencing.

Has anyone ever seen anything like that?

Maybe this platform just boots up a webserver and displays on a webpage?

view this post on Zulip Luke Boswell (Mar 13 2024 at 01:34):

I imagine having like an VS Code extension open that displays a window next to my code as I develop it, and I can see in real time what the tasks are doing.

view this post on Zulip Richard Feldman (Mar 13 2024 at 01:44):

hm, I think showing a graph would require language-level integration

view this post on Zulip Richard Feldman (Mar 13 2024 at 01:44):

because as soon as you hit an await, the platform can't possibly know what comes next until the callback function has actually executed

view this post on Zulip Richard Feldman (Mar 13 2024 at 01:45):

the compiler can look at the source code for the callback and analyze which code paths can potentially result in which other tasks, and draw a graph from there

view this post on Zulip Richard Feldman (Mar 13 2024 at 01:45):

but the platform can't see the source code, so all it knows is the 1 path that actually got taken, not what the alternatives were

view this post on Zulip Luke Boswell (Mar 13 2024 at 01:50):

Richard Feldman said:

hm, I think showing a graph would require language-level integration

Like a language server maybe?

view this post on Zulip Richard Feldman (Mar 13 2024 at 02:03):

could be!

view this post on Zulip Luke Boswell (Mar 13 2024 at 02:05):

Glad to hear it's possible to build. Would that be useful though? Has anyone ever seen anything like that used elsewhere?

view this post on Zulip Richard Feldman (Mar 13 2024 at 02:07):

it might be useful, although I'm not sure how much of a problem this is currently :big_smile:

view this post on Zulip Richard Feldman (Mar 13 2024 at 02:07):

it doesn't seem to be something I notice a lot of questions about!

view this post on Zulip Brendan Hansknecht (Mar 13 2024 at 02:14):

I think async is so common nowadays that it isn't too important

view this post on Zulip Brendan Hansknecht (Mar 13 2024 at 02:15):

That said, the task mutation is a bit more than simple async in some cases

view this post on Zulip Anton (Mar 13 2024 at 10:36):

Specifically how with roc we are building up a data structure which describes what to do, and then passing that off to the platform to actually do the thing... so chaining tasks doesn't actually do anything until they are executed.

Can you add that to the tutorial @Luke Boswell? I also think that is worth mentioning.

view this post on Zulip Eli Dowling (Mar 14 2024 at 22:34):

Generally it's referred to as "cold tasks" it'd be good to mention that c#, JavaScript, and maybe java(I'm not quite sure) all have "hot tasks"

view this post on Zulip Brendan Hansknecht (Mar 14 2024 at 22:42):

Is async in rust with Tokio cold tasks? Do you know? I think we theoretically will be compiling to basically the same thing when we switch to effect interpreters.

view this post on Zulip Eli Dowling (Mar 14 2024 at 22:44):

Something that would be really cool is some kind of task tracer:
Ocaml's effect system has a few, one being this:
https://github.com/ocaml-multicore/eio-trace
It would be pretty cool if roc could generate similar traces that show a kind of flamegraph all the effects that were run.

view this post on Zulip Eli Dowling (Mar 14 2024 at 22:50):

@Brendan Hansknecht
My brain is telling me yes, but I'm not totally sure.

If you make an Async block it doesn't do anything until you run it on a thread right? And you can do joins, and awaits and everything inside without actually starting the task, you are just bringing up a big future data type, right?

Whereas in JS for example, you can only have a promise when a task is actually running.

view this post on Zulip Eli Dowling (Mar 14 2024 at 22:50):

I found this:
https://users.rust-lang.org/t/similarities-between-c-async-await-and-tasks-and-rust-async-await-and-futures/52771/2

view this post on Zulip Eli Dowling (Mar 14 2024 at 22:50):

They say the same thing, rust has cold Async.

view this post on Zulip Luke Boswell (Mar 14 2024 at 22:54):

I'm trying to research "hot" and "cold" tasks, but haven't much. Are these similar to hot and cold observables?

view this post on Zulip Luke Boswell (Mar 14 2024 at 22:54):

I'm not sure the observer pattern really fits in this context

view this post on Zulip Luke Boswell (Mar 14 2024 at 22:57):

Found something that looks right;

Understanding tasks and task generators
In the paper about asynchronous programming model in F# [1] (written by Don Syme, myself and Dmitry Lomov), we mentioned three possible options that can be used for implementing the asynchronous programming model:

view this post on Zulip Eli Dowling (Mar 14 2024 at 23:40):

Yeah, maybe it's a more common term in the f# world, but I have heard it mentioned elsewhere.
I know somewhere I've read a list of examples where what you're used to in hot tasks won't happen with cold tasks
.
The simplest one would be:

a=
  doThing= Stdout.printLine "hi"
  100

In a hot task model, you would expect this to print "hi", In a cold task model like rock this does nothing.

view this post on Zulip Eli Dowling (Mar 14 2024 at 23:44):

I'm very used to it now, but I do remember when I was first learning f sharp I had a bunch of times where I thought something should have started and it just didn't run and it was very confusing

view this post on Zulip Luke Boswell (Mar 14 2024 at 23:57):

This is really good context or background to explaining tasks in the tutorial as Anton mentioned. It will be good to reference familiar concepts from other languages.

view this post on Zulip Richard Feldman (Mar 15 2024 at 00:06):

maybe...I have to admit, I have a bit of a concern that we're talking about solving a problem that doesn't have any symptoms :sweat_smile:

view this post on Zulip Richard Feldman (Mar 15 2024 at 00:07):

is there a reason we suspect beginners are currently having trouble understanding how tasks work in Roc?

view this post on Zulip Richard Feldman (Mar 15 2024 at 00:15):

I actually try to minimize referring to features of other languages, for a few reasons:

view this post on Zulip Richard Feldman (Mar 15 2024 at 00:17):

I think the most useful ways to refer to things in other languages are for really broad things that appear in multiple languages, e.g. "tasks are kind of like Promises, but they don't do anything right when you create them" or "platforms are kind of like frameworks but they are in charge of more things, such as memory management and all I/O operations"

view this post on Zulip Luke Boswell (Mar 15 2024 at 02:04):

Richard Feldman said:

maybe...I have to admit, I have a bit of a concern that we're talking about solving a problem that doesn't have any symptoms

is there a reason we suspect beginners are currently having trouble understanding how tasks work in Roc?

I should have provided more background to explain why I think it would be beneficial to explore this idea. I'm not trying to highlight a specific issue; I wanted to explore if there is an opportunity to improve and help people build a mental model.

Getting comfortable putting the lego's together await attempt try loop map fromResult ok err has taken me a while. The first time I wrote two Stdout.line to print to the screen, I was confused about how/why to chain the Tasks together.

Understanding Tasks is fundemental to using roc, and I had to build up a mental model and gradually refine what I understand a Task is and how it works.

My thought was how can I help others to get to a working mental model without needing to spend as much time as I have.

I think the design of Tasks is great - they are really good to work with - so I don't think there is a problem with the design.

I'll sit on these ideas for now and come back to this another time.

view this post on Zulip Brendan Hansknecht (Mar 15 2024 at 02:12):

I do think that !, better compiler errors, and some bug fixes around requiring Task.loop will greatly reduce the overhead here. So I think as time passes, there will be less and less to explain.

view this post on Zulip Eli Dowling (Mar 15 2024 at 02:36):

I actually mostly agree with you @Richard Feldman. I would say in this case people coming from almost any mainstream language would only have experience with hot tasks, and therefore have incorrect assumptions. I do think that it's worth highlighting, that there is a fundamental difference in how they execute. I can obviously only speak to my one experience learning this (if only we could try learning things again) but I would have benefited from being told up front "your assumptions about what a task is doing are likely wrong"

view this post on Zulip Richard Feldman (Mar 15 2024 at 02:37):

that all makes sense! :100:

view this post on Zulip Noel R (Mar 30 2024 at 19:18):

Ah, this conversation is making me feel better already: I'm having trouble understanding how tasks work in Roc. In my attempts I get the wrong results most of the time, which shows that my mental model of Tasks (and Results and their relationship?) is not good at all.

So, for me at least, it has been a pain point. A fun one, but still :smile:

view this post on Zulip Hristo (Apr 01 2024 at 09:13):

As a Roc/Elm beginner (that I still am), my initial encounter with Tasks was not straightforward, so I'll try to share my perspective, in case it might be of help.

Initially, the crude mental model that I leaned towards was a parallel to how async-await works in languages like Python and JavaScript in terms of being able to chain multiple async functions together, and then do error-handling on top of that correspondingly. In Roc, my conceptual (yet quite far from perfect) understanding is that tasks are currently only platform-specific concepts. Basically, by defining a task (or a chain of tasks), one indicates to the platform what the tasks constitutes of, ahead of execution time. The execution itself happens at a later point in time that's relevant from the perspective of the platform itself, which is after the Roc code has handed over the instructions; and the platform itself implements the lower-level details, pertaining to the how of execution, including scheduling, result gathering etc.

I happened to listen to what I believe is quite a relevant Software Unscripted podcast episode yesterday, and I hope the admins wouldn't mind my posting an excerpt from the transcript here (the excerpt is from page 5/20). I believe this is a much better summary than what my attempt could've made justice to tasks, but I decided to offer my beginner's perspective nevertheless, as a juxtaposition (and also, in case, it'd be helpful to the thread if more experienced Roc/Elm programmers would have time to indicate inaccuracies in my particular perspective):

Richard Feldman:
Whereas an Elm, I might do the same thing with a task. Like I have a function
that returns a task and just like promises, tasks can be chained together. They
can represent like, I want to do an HTP request and then I want to do this other
thing, yada, yada. But the difference is that in the Elm function, when I return it,
yeah, it's going to return this task, and that task sort of represents the HTTP
request that I want to have done. The difference is just that if I call the
JavaScript function a hundred times, it's going to fire off 100 HTTP requests.
If I call the Elm function 100 times, it's still just going to return this value that
describes, "Hey, I want an HTTP request to happen here." It's not going to fire
off 100 times. It's still just going to be, well, I got this task back. So the
difference between task and promise is really just a question of side effects
versus managed effects. Side effects being right when you make it, it does the
effect. Managed effect being, this is just a description of the effect that I want
the run time to do in the future.
...
And the follow-up to that is that I think people might have an idea in their
head that doing pure functional programming is going to be radically different in
terms of what it feels like day-to-day than something like imperative
programming when it comes to effects. But actually it's really not that different.
You see a lot of things that instead of returning promise, they return task, but
you chain them together in basically the same kinds of ways. The only difference
is when they actually get executed by the run time versus immediately.

view this post on Zulip Noel R (Apr 08 2024 at 14:56):

Thank you @Hristo for sharing the link to that episode; it was helpful.

Going back to what I said about having trouble understanding Tasks: as I make progress, I've been reflecting on the errors I made before, and the beliefs that led to them.

Thinking imperatively
My head kept reading patterns such as result <- (someFunction "arg") as "ah! we're assigning the result of the task to result, so we should have a string there, right?".

It took some time to understand that I'm writing a recipe to handle the result of something that will run in a parallel universe (the platform). What comes out of that portal is not a string: it's _something_ that could be a string, but all possible cases must be considered.

Syntax
The backpassing and pipe operators in particular were confusing to me. In my first attempts, I did all sorts of weird stuff that resulted in wrong arguments passed, type mismatches and panics. Writing the same fragments in sweet / sugar-free versions helped a lot.

I think I would have benefited from having more examples in both sugary and desugared versions, considering that the docs are written with syntax sugar.

Overconfidence
Although I was familiar with the concepts of functional programming, I didn't have hands-on experience —and yet I expected to understand things just from glancing at examples. I was wrong.

view this post on Zulip Luke Boswell (Apr 08 2024 at 15:01):

Thank you for sharing @Noel R. Have you seen the Chaining Syntax design proposal? You may find that interesting, and I would be keen to know what you think.

I've been working on implementing it recently and I think it will be a nice improvement to not use backpassing and have a more direct statement loooking syntax for Tasks.

view this post on Zulip Noel R (Apr 09 2024 at 00:45):

I hadn't! I really like that proposal. Reading it, I realized that the visual noise added by the {} <- fragments is significant. The eyes cannot simply scan vertically to understand the outline of the code, so additional effort must be made to first decide where the line starts, and then decode its meaning. For a single line it's trivial but when scanning code my eyes really appreciate the clean edge.

A concern I'd have with using the assignment operator = for two purposes (backpassing and assignment) is that, if I understand correctly, it would be impossible for someoneone reading code to desugar just from the text on screen without fully understanding the code —this could be an issue for beginners like myself.

So, I'm wondering if we can get the best of both worlds by keeping the backpassing operator (not sure if operator is the right word :thinking: ), while applying the rest of the proposal.

For example:
content <- File.readUtf8! path instead of
content = File.readUtf8! path

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 01:28):

I think a main goal of ! is accepting that generally speaking, users don't really need to think about the await and it's underlying closure.

They just need to know that ! means async function.

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 01:29):

Backpassing is a fundamentally more powerful syntax, while ! is simpler and hides more to avoid confusion and details that many users don't need.

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 01:30):

As for desugaring. <- vs = should make no difference. The desugaring happens due to the !. So if you scan for those, you can desugar anything.

view this post on Zulip Przemek Kitszel (Apr 09 2024 at 03:02):

Luke Boswell said:

Thank you for sharing Noel R. Have you seen the Chaining Syntax design proposal? You may find that interesting, and I would be keen to know what you think.

I've been working on implementing it recently and I think it will be a nice improvement to not use backpassing and have a more direct statement loooking syntax for Tasks.

That's awesome!
Fits perfectly into "let me write core of the $thing in pure functional way, and glue it synchronously using similar syntax".
I'm also for retiring <- syntax.

The only change I will suggest: instead of taking ? character for something that typical code will not do, consider changing it so you have (! SomeTask.await), aka the same !, but with parens and argument.

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 03:17):

Can you explain your suggestion more? Maybe give an example with args?

view this post on Zulip Przemek Kitszel (Apr 09 2024 at 06:24):

(in my suggestion)main = Stdout.line! "Hello, World" could be rewritten as main = Stdout.line (! Task.await) "Hello, World". Parenthesis makes it more obvious, but could not be removed (same arguments that go against curring).

Looks bloatier that ? Task.await - just a bit, but we don't spend the valuable ? character that could be used for future extensions.

view this post on Zulip Luke Boswell (Apr 09 2024 at 07:43):

I kind of like having the more concise syntax that is in the proposal. Here is an example I've been looking at earlier.

# today's syntax w/o backpassing
main =
    Task.await (Stdout.line "Ahoy") \{} ->
        Task.await (Stdout.line "there") \{} ->
            Task.ok {}
# with chaining syntax
main =
    Stdout.line! "Ahoy"
    Stdout.line! "There"

    Task.ok {}

Both of these print out Ahoy There to stdio, but I think the second is much easier to follow at a glance.

view this post on Zulip Luke Boswell (Apr 09 2024 at 07:44):

The former will still be valid as the ! is just syntax sugar and unwrap to the exact same code.

view this post on Zulip Noel R (Apr 09 2024 at 14:59):

I think a main goal of ! is accepting that generally speaking, users don't really need to think about the await and it's underlying closure.

I'm not sure if I would actually want the existence of the await and underlying closure to be hidden: it seems to me that async code and task handling are fundamental to the language.

As a learner, I'd rather keep that in mind, but having a syntax alternative that's nicer to write and read is great. I'm not sure I understand the power of backpassing so I can't comment on that.

This example can be found in the proposal:

main =
        Stdout.line! "one"
        Stdout.line! "two"

Would that be valid? From Luke's example above, and considering that it's just syntax sugar, I think it wouldn't: the last line should not have the ! or it should be followed by something that produces a task.

If that's correct, then it's one of those cases that make me think that we can't hide the existence of tasks and async behavior from users. The readability can improve for sure and, in that line, I really like this proposal :ok:

I'm pretty new to Roc and have zero experience in language design, so take all of this as newbie opinion.

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 15:01):

That is allowed. It is a special exception to the sugar. A ! on the final task of a function is just ignored. It enables users to write consistent code.

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 15:03):

Also, by hide, I mostly mean we can teach the simple thing first and if that is enough for users, they can just play with the language. If not, we can dive in and explain the sugar to give them more understanding.

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:16):

Brendan Hansknecht said:

That is allowed. It is a special exception to the sugar. A ! on the final task of a function is just ignored. It enables users to write consistent code.

as a minor note, I'd like to try not allowing this at first and seeing how it goes (maybe with a nice compiler error if you include it but shouldn't; that's a very easy scenario to detect)

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:16):

a downside of optionally allowing it that I hadn't thought of originally is that there's no way for roc format to enforce the convention that you should always have it

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:16):

because roc format doesn't know the types of the program, so it can't know what is and isn't a task

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:17):

and it certainly can't go around adding ! to every function call at the end of defs, because that would not work out most of the time :sweat_smile:

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:17):

so I think we should do the experiment of not allowing the optional ! at the end and see how it goes

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:17):

because it's trivial to have a consistent style with that design

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:25):

actually we could have it be a warning, come to think of it

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:25):

(with a helpful message)

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 15:42):

I get the reasons, but it does lose a solid chunk of the benefit of ! in my opinion. Will make people question it more and pull back the covers sooners.

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:57):

hm that's a good point

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:57):

I guess a related question is: how much of a concern is it if people use it inconsistently? :thinking:

view this post on Zulip Richard Feldman (Apr 09 2024 at 15:59):

like how annoyed would I be if a code base formatted using roc format sometimes did and sometimes didn't use the trailing !

view this post on Zulip Richard Feldman (Apr 09 2024 at 16:00):

hm, maybe the better experiment to run is whether roc format not enforcing it is actually a problem in practice

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 16:03):

Also, I guess we have 3 possible styles:

main =
   Stdout.line! "A"
   Stdout.line "B"
main =
   Stdout.line! "A"
   Stdout.line! "B"
main =
   Stdout.line! "A"
   Stdout.line! "B"
   Task.ok {}

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 16:04):

First and last valid for sure. Middle depends on the optional last !

view this post on Zulip Richard Feldman (Apr 09 2024 at 16:25):

yeah whenever I write out nontrivial examples I prefer the middle style

view this post on Zulip Richard Feldman (Apr 09 2024 at 16:26):

it's kinda like how in Rust I always put ; at the end of things that return () even when it's at the end of the expression and I don't need to

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 16:26):

Same. I think I actually like the first example the least. Even though I know it isn't inconsistent, it feels inconsistent.

view this post on Zulip Richard Feldman (Apr 09 2024 at 16:27):

and I guess actually that's an interesting precedent; rustfmt doesn't enforce trailing semicolon consistency (probably because that would also require type information) and it seems totally fine in practice

view this post on Zulip Noel R (Apr 09 2024 at 17:32):

as a minor note, I'd like to try not allowing this at first and seeing how it goes (maybe with a nice compiler error if you include it but shouldn't; that's a very easy scenario to detect)

I like not having the exception since having an exception adds a conditional meaning to !.

From the three styles, I'd lean towards 3 since it's more explicit (and 1 looks weird...)

view this post on Zulip Luke Boswell (Apr 09 2024 at 22:22):

Brendan Hansknecht said:

I get the reasons, but it does lose a solid chunk of the benefit of ! in my opinion. Will make people question it more and pull back the covers sooners.

This could be a good thing though. It's convenient for the common case, but still encourages people to understand "why do I need to add the Task.ok {}?"?

I've had a couple of cracks at supporting the optional last place thing. It's probably doable, but it will require some changes in the parser. I think the issue is where does an expression end, at the moment an expression can continue across a newline at the same indent, but that means it's hard to detect a final expression like this.

view this post on Zulip Luke Boswell (Apr 09 2024 at 22:23):

So the PR I'm working on doesn't have that feature yet.

view this post on Zulip Brendan Hansknecht (Apr 09 2024 at 22:39):

Personally, I have no issues with either of these syntaxes:

main =
   Stdout.line! "A"
   Stdout.line! "B"
main =
   Stdout.line! "A"
   Stdout.line! "B"
   Task.ok {}

This is the only syntax that I think will be inconsistent/confusing:

main =
   Stdout.line! "A"
   Stdout.line "B"

view this post on Zulip Hristo (Apr 09 2024 at 23:01):

For completeness, could somebody provide an example of how (a relatively non-trivial chain of) error handling might look like (in case there'd be distinctive differences) with the proposed alternative syntax?
Thanks!

view this post on Zulip Richard Feldman (Apr 09 2024 at 23:06):

I don't think theTask.ok {} design is nice in longer examples :big_smile:

view this post on Zulip Richard Feldman (Apr 09 2024 at 23:06):

it looks fine here but not in a big chunk of code that's doing a bunch of I/O

view this post on Zulip Luke Boswell (Apr 09 2024 at 23:09):

I agree, I think it will be nicer. Just wanted to add that it may not be so bad.

@Hristo

view this post on Zulip Luke Boswell (Apr 09 2024 at 23:15):

main = run |> Task.onErr \err -> Stdout.line "ERROR: $(Inspect.toStr err)"

# task that bundles errors
# into a single tag union
run : Task {} _
run =

    # a couple of statements
    Stdout.line! "Ahoy"
    "There" |> Stdout.line!

    # check jq is available
    Cmd.new "jq"
    |> Cmd.arg "--version"
    |> Cmd.status
    |> Task.mapErr! UnableToCheckJQVersion

view this post on Zulip Notification Bot (Apr 10 2024 at 00:45):

8 messages were moved from this topic to #beginners > Error handling with tasks by Brendan Hansknecht.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 01:03):

To be complete, here are the three versions of a more complex example from the design proposal. I just added in error handling. I internationally made the error handling more explicit to show the apis. Also, I made one function where there is no way to end all branches with bang.

Unnecessary ! at end

Minimal ! possible

Explicit result at the end

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 01:04):

If anything, this makes me realize that consistantly sticking to any one approach on all functions is unlikely to work well.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 01:05):

I would use the unnecessary ! for storeEmail and would avoid it in withRetries.

my perference

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 01:09):

For completeness:

current syntax

view this post on Zulip timotree (Apr 10 2024 at 02:02):

This reminds me of the discussion of "Ok-wrapping" for Rust's try {} blocks feature. With Ok-wrapping, the final expression in a try block is automatically wrapped in Ok(). (And if it's omitted, then it defaults to Ok(()), the equivalent of Roc's Task.ok {}.) If Roc were to adopt some kind of Task.ok-wrapping, I think it would force a more consistent style where you always use ! for trailing function calls.

view this post on Zulip timotree (Apr 10 2024 at 02:10):

With ok-wrapping, Brendan's example might look like this

withRetries : Task ok err, U64 -> Task ok err
withRetries = \task, attempts ->
    res = Task.toResult! task
    when res is
        Ok x -> x
        Err e ->
            if attempts == 0 then
                Task.err! e
            else
                withRetries! task (attempts - 1)

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 02:28):

Interesting. So it makes the ! required on all paths even the error path.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 02:29):

Ah, though it has Ok x -> x as well. Cause x is of the ok result type and will be wrapped automatically.

view this post on Zulip timotree (Apr 10 2024 at 02:49):

Exactly. And if Task.err! reads weirdly, you might create an alias like throw! or bail! to make it read more like an imperative command

view this post on Zulip witoldsz (Apr 10 2024 at 08:23):

Luke Boswell said:

Thank you for sharing Noel R. Have you seen the Chaining Syntax design proposal? You may find that interesting, and I would be keen to know what you think.

This is so impressive! I'd like to add that F# has gone similar way, but with one distinct difference: the meaning of the ! syntax (it is kind of similar to the new syntax design proposal above) can vary depending on the computation expressions and there are some included in the standard library and also by 3rd parties.

Example:

let doThingsAsync url =
    async { // <-------- this is the computation expression defining the meaning of ! within
        let! data = getDataAsync url
        do! sendDataAsync data
    }

So, the ! is for chaining async tasks, but in other environment they do other things, basically it all revolves around chaining monads and/or applicatives.

I am not suggesting the Roc should have something similar, but F# is not very popular, but it has lots of interesting (i.e. inspiring) stuff, sometimes I can't help but feel obliged to share it to wider audience :)

Computation expressions in F# provide a convenient syntax for writing computations that can be sequenced and combined using control flow constructs and bindings. Depending on the kind of computation expression, they can be thought of as a way to express monads, monoids, monad transformers, and applicative functors. However, unlike other languages (such as do-notation in Haskell), they are not tied to a single abstraction, and do not rely on macros or other forms of metaprogramming to accomplish a convenient and context-sensitive syntax.

view this post on Zulip Przemek Kitszel (Apr 10 2024 at 08:44):

additional reason for allowing (and preferring) a trailing ! is usage with git, when you add a new action as a new last, it's best when you don't touch previous line (think git blame).

sometimes it would be beneficial to remove the trailing !, which would mean 'no more code expected after this line'

Such convention works fine for C enums, you always add a , at the end, but don't if it's something intended to be the last one (size/count/max).

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 14:32):

@witoldsz I think the original discussion and design was actually more similar to the F# version.

Something like

with Task.await
    Stdout.line!
    ...

I think in the end the preference leaned more towards it probably only being needed for a few types with Roc's current design (Task and Result for example). So it would be nicer to make it less noisy and just work. There was also discussion of making it just work with any andThen style function that is specified for a type. But that would only be added with significant demand due to the extra complexity.

For now it is just being added as special magic for Task.

view this post on Zulip witoldsz (Apr 10 2024 at 15:05):

Yes, I do find it attractive that the current proposal focuses on ! as a specialized construct just for Task.await, so the entry point is so low that pure-fp Roc stands up to an even fight with folks like Ruby or Python in scripting capabilities.

Now, I do notice the with ... syntax is more like computation expressions of F# but as far as I can see the Roc proposition is not as powerful, especially when it comes to applicatives. Applicatives are superb for example when you want to validate a form and do not want to stop on a single error, but continue with all checks and collect errors. In F# you can do it like this:

        validate {
            let! country = Check.optional (asString >=> isCountryCode) $"{field}.country" (decode json "country")
            and! city = Check.optional (asString >=> Check.String.betweenLen 2 100) $"{field}.city" (decode json "city")
            and! postCode = Check.optional (asString >=> isPostCode) $"{field}.postCode" (decode json "postCode")

            and! streetAndNumber =
                Check.optional
                    (asString >=> Check.String.betweenLen 1 250)
                    $"{field}.streetAndNumber"
                    (decode json "streetAndNumber")

            return
                { Country = country
                  City = city
                  PostCode = postCode
                  StreetAndNumber = streetAndNumber }
        }

Notice: validate CE comes from 3rd party library. The result is either a record or a collection of errors.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 15:41):

That is very similar to our record builder syntax, which I think was built for this purpose. Recently there was an example of it in #show and tell > Weaver: record builder CLI parsing library

view this post on Zulip Jasper Woudenberg (Apr 10 2024 at 16:50):

Wait, clarifying question: is record builder syntax it's own thing, separate from backpassing?

view this post on Zulip Anton (Apr 10 2024 at 17:02):

Yes

view this post on Zulip Richard Feldman (Apr 12 2024 at 19:54):

@Jasper Woudenberg we ended up changing the syntax slightly, but here is the original design doc: https://docs.google.com/document/d/1Jo9nZCekkoF6SaDcRqPqoPcgPaAAvlNZC7v3kgVQ3Tc/edit?usp=sharing

view this post on Zulip Vinicius Ataide (Jun 07 2024 at 13:53):

I'm really excited to see the new language constructs introduced in the latests version. looks like there's no pushback operator anymore for Task.await right

view this post on Zulip Anton (Jun 07 2024 at 14:00):

I think you mean the "backpassing" (<-) operator?
It's still there but we would like to remove it in the future


Last updated: Jul 05 2025 at 12:14 UTC