Stream: beginners

Topic: Don't Get Backpassing


view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 09:26):

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.

view this post on Zulip Anton (Oct 21 2023 at 09:56):

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)"

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 10:03):

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.

view this post on Zulip Anton (Oct 21 2023 at 10:16):

Well said!

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 10:23):

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.

view this post on Zulip Declan Joseph Maguire (Oct 21 2023 at 10:25):

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.

view this post on Zulip Richard Feldman (Oct 21 2023 at 12:36):

I explained it in a different way, including desugaring, here - does this help? https://youtu.be/6qzWm_eoUXM?si=nAIzIRt9H5M5plYh&t=1870

view this post on Zulip Sven van Caem (Oct 21 2023 at 13:20):

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.

view this post on Zulip Sven van Caem (Oct 21 2023 at 13:24):

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.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 14:59):

Out of curiosity was scala at all a motivation for this feature?

view this post on Zulip Lakin Wecker (Oct 21 2023 at 15:00):

I'm assuming not, but worth asking, because it looks very similar to scala's treatment of flatMap and map with for and <-

view this post on Zulip Brendan Hansknecht (Oct 21 2023 at 15:03):

Nope

view this post on Zulip Brendan Hansknecht (Oct 21 2023 at 15:04):

I don't think any of us working on roc know scala (past very basic interaction with it)

view this post on Zulip Anton (Oct 21 2023 at 15:04):

I do :)

view this post on Zulip Richard Feldman (Oct 21 2023 at 15:05):

I was a professional Scala developer for about 4 months, more than a decade ago :sweat_smile:

view this post on Zulip Lakin Wecker (Oct 21 2023 at 15:05):

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

view this post on Zulip Lakin Wecker (Oct 21 2023 at 15:06):

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.

view this post on Zulip Richard Feldman (Oct 21 2023 at 15:06):

Scala was not a motivation for backpassing at all; it started with do notation as inspiration, but wanting it to be purely syntax sugar

view this post on Zulip Richard Feldman (Oct 21 2023 at 15:06):

I think Scala got it from Haskell too, but I could be wrong!

view this post on Zulip Lakin Wecker (Oct 21 2023 at 15:06):

Interesting that you arrived at a very similar looking solution. No idea if it's implemented in the same way

view this post on Zulip Brendan Hansknecht (Oct 21 2023 at 15:08):

Ours is just syntax sugar, so probably not. Just a lambda written different.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 15:10):

I'm always afraid to keep giving example of how things are done in other languages in these streams as it seems off-topic

view this post on Zulip Brendan Hansknecht (Oct 21 2023 at 15:12):

: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.

view this post on Zulip Lakin Wecker (Oct 21 2023 at 15:13):

I put it in an offtopic topic

view this post on Zulip Tad Lispy (Nov 17 2023 at 20:29):

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.

backpassing.png

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.

view this post on Zulip Brendan Hansknecht (Nov 17 2023 at 21:56):

Yeah, that's a pretty great explanation


Last updated: Jul 06 2025 at 12:14 UTC