I admit it I still don't get backpassing. Basically every other part of Roc is easy to understand, but backpassing is mysterious to me. It certainly makes the code a lot prettier, but I just have absolutely no mental model for it.
By contrast |>
is totally trasparent to me, even though it also gets up to some nonsense with the order things work in - it basically just acknowledges that a ton of big functions are pragmatically understood as acting ON one of the arguments WITH the rest, with that object passing from function to function. It's function composition. It's a pipeline. It's in the name. I can immediately grok it.
But I've read the Roc tutorial page over and over (and now the roc for elm page)(never touched elm though) and it's just not clicking. If you gave me a perfectly precise description of how backpassing desugars, and some code for me to desugar manually, I give myself an 80% chance of nailing it.
Yeah, it's a pain point. I think it would be helpful to explain backpassing using a more familiar function then Task.await
.
Perhaps we could also show an indented version that is more similar to the desugared version to make it easier to understand:
task =
_ <- await (Stdout.line "Type something press Enter:")
text <- await Stdin.line
Stdout.line "You just entered: \(text)"
Yeah the fact that the explanations already build on await, a rather abstract concept, makes it extra hard.
I think I (and others) need a problem whose internal structure is immediately understandable, which is poorly expressed by conventional notation, but which is sorta "isomorphic" to the backpassed version. A problem which, when articulated the way anyone instinctively would, is already structured like a backpass statement/chain.
Well said!
To tighten it further, this problem should ideally be something anyone could understand, not just those who'd learnt programming. A "natural" problem, not merely a "technical" one.
Anton said:
Well said!
I've got a mild obsession/compulsion with articulating ideas, especially those that are simple but resist expression. Given that doing so was both the topic of my prior comment and inherent to the act of formulating it, I tried real hard to nail it.
I explained it in a different way, including desugaring, here - does this help? https://youtu.be/6qzWm_eoUXM?si=nAIzIRt9H5M5plYh&t=1870
Declan Joseph Maguire said:
Yeah the fact that the explanations already build on await, a rather abstract concept, makes it extra hard.
I think I (and others) need a problem whose internal structure is immediately understandable, which is poorly expressed by conventional notation, but which is sorta "isomorphic" to the backpassed version. A problem which, when articulated the way anyone instinctively would, is already structured like a backpass statement/chain.
I remember I had an easier time figuring out what backpassing did in an example using Result.try
instead of Task.await
, precisely because I could read and understand Result.try
's implementation before figuring out how it interacted with backpassing. In contrast, Task.await
is necessarily opaque, so explaining backpassing in terms of it requires you to solve for two unknowns at a time.
I also remember realizing at some point that I could use backpassing in to define a function passed to List.map
. It's definitely an impractical example, cause if anything it made the code harder to understand, it did kinda help make it click for me that the rest of the body after the backwards arrow really just becomes its own function that, in this case, the List.map
can just do whatever it wants with. I'm not sure this specific example will be useful to help teach the concept in general, but I remember thinking it was a noteworthy moment.
Out of curiosity was scala at all a motivation for this feature?
I'm assuming not, but worth asking, because it looks very similar to scala's treatment of flatMap
and map
with for
and <-
Nope
I don't think any of us working on roc know scala (past very basic interaction with it)
I do :)
I was a professional Scala developer for about 4 months, more than a decade ago :sweat_smile:
cats-effect in scala makes heavy use of this with all of it's monads to chain effects together in a very similar way. The only syntactical difference is that you have to start with afor
and end with a yield
, but in the middle the syntax is nearly identical and uses the same <-
and achieves the same niceness of avoiding the pyramid of doom
And scala has a bit of extra subtyping magic to make effects that aren't the same, but are compatible be able to work together, so you can chain effects in many different ways.
Scala was not a motivation for backpassing at all; it started with do
notation as inspiration, but wanting it to be purely syntax sugar
I think Scala got it from Haskell too, but I could be wrong!
Interesting that you arrived at a very similar looking solution. No idea if it's implemented in the same way
Ours is just syntax sugar, so probably not. Just a lambda written different.
I'm always afraid to keep giving example of how things are done in other languages in these streams as it seems off-topic
:shrug: probably not a big deal unless the other discussion is live and this kinda cuts it apart. That said, we can always move messages around. At the same time, threads are cheap, so feel free to just make a new one whenever.
I put it in an offtopic topic
Thank you for this thread. It helped me understand backpassing. I'll try to explain how I think about it. Please correct me if I'm wrong.
Conceptually backpassing is like passing a callback, turned into an assignment operation, as it involves a function that takes a callback. The name on the left side of the arrow is a parameter for the callback. Every line after the one with the <-
operator becomes the body of the callback. What’s on the right side of the arrow is a function call, but with last argument omitted. The callback function will be passed as this last argument. The most important thing is that everything that follows the line with the arrow will be evaluated inside the callback! That's why order of backpassing expressions is important (unlike assignments). Essentailly it creates a nesting of closures.
Let's consider the following.
value <- produce input
transformed = doSomethingWithThe value
doSomethingElseWith transformed
It is exactly equivalent to this:
produce input \value ->
transformed = doSomethingWithThe value
doSomethingElseWith transformed
Here value
is the parameter, produce
is a named function that takes some input
as its first argument and a callback as the second argument. The following two lines with doSomething...
are the body of this callback.
So, like @Brendan Hansknecht said before, backpassing is just sugar and no magic. The benefits are lack of indentation and more intuitive syntax. I like it!
Here is an executable example without using tasks and await
.
Yeah, that's a pretty great explanation
Last updated: Jul 06 2025 at 12:14 UTC