I've known since first reading the tutorial that backpassing is very useful in sugaring (simplifying the syntax of) asynchronous callbacks (specifically reducing indentation). However, I just saw an example of backpassing being used to chain synchronous contexts together. When else is backpassing useful?
It seems like a much more powerful (or FP fundamental) feature than I'd originally thought!
it is, but that also makes it tricky to talk about in a clear way
a decent intuition is that it is about the ordering of operations. first do this, then that, then that
this is achieved with a data dependency: the x <-
part of the backpassing relies on the value of the <- ....
part being available
so now think about "ordered" things: threading a state through a computation, updating a random seed, printing lines to the console in order
here the output of the computation relies on the order. it doesn't in most parts of a functional program: 1 + 2 + 3
can be evaluated in arbitrary order
but I guess the elm way of understanding this is: is there a function andThen : m a -> (a -> m b) -> m b
for this type
(it must behave in a certain way, too, not any implementation will do)
... is ->
just monadic =
?!
I'm finally grasping monads and it looks like ->
is more of their chaining operator than |>
...
Maybe more of a "raw" chaining operator?
Your explanation makes sense, but that one part about "relies on the value of the <- ....
part being available" confuses me - wouldn't that just be =
?
JanCVanB said:
... is
->
just monadic=
?!
Yes! Although it can be used in a slightly more flexible way because the monadic bind operator (known as >>=
or andThen
or bind
or chain
in different languages) has the exact type signature m a -> (a -> m b) -> m b
while IIRC in Roc you can use backpassing also with functions of the format m a -> (a -> x b) -> x b
(continued) It seems like we're waiting on a variable nested INSIDE the right-hand side's context to become available, rather than the evaluation itself.
It might be intuitive to think of all of these situations as callbacks. You can only ever evaluate a callback if the parameter to the callback was evaluated first.
How exactly you go from 'the thing you have before' to 'the precise parameter that is passed into the callback' (and how often the callback is executed exactly) depends on the function you are using. List.joinMap
, Task.after
, Result.try
are three examples
List.joinMap
: Run the callback once for each element in the input list, and concatenate the resultsTask.after
: Run the input task to completion, and once it is finished (without error) run the callback with the outcome.Result.try
: Run the callback only if the input is Ok val
, otherwise short-circuit with the error value immediately.Right, but the variable passed as the parameter isn't the right-hand evaluation itself, it's a variable within that evaluation's context.
Correct
ex. Result.try
internally contains logic that turns a Result a err
into a variable of the type a
that the callback expects as input parameter type.
:D somehow that helps me!
:green_heart:
Is it only useful in synchronous situations when the internal state is intended to be hidden from the outside?
Hooray! 🥳
Otherwise we could just use =
and pass that internal state around in a more complex return value.
It is useful in any situation where it makes sense to keep track of something extra 'to the side' of plain sequencing.
...without that side thing being visible/accessible in the calling context?
There is a trivial implementation that does nothing extra which indeed simply uses =
internally. It is not often useful in practical situations though; more a theoretical curio.
There are many situations in which it is already helpful just to hide the following alternative:
myfun = \input ->
let (val, state) = foo defaultState input
let (val2, state2) = bar state val
let (val3, state3) = baz state2 val2
qux state3 val3
:point_of_information: this is actually what is going on behind the scenes in the RNG example
But besides that: There are indeed many situations where it is extra useful because you can prevent the internals from being accessible from the outside. Effect
and Task
are good examples of this.
Yes! This reminds me of my RNG example - jinx! You beat me to it haha
If a calling context ever wanted to return the internals, does using any backpassing preclude/prevent that? Or can it just remove the last backpassing call to extract the final internals?
For example, in the above snippet, if we switched to using backpassing for any of those foo
/bar
calls, can myfun
return any state
s at all or just val
s?
It could only access val
s
Okay, thank you. That answers a question I couldn't formulate until now.
So backpassing/monadic composition alone hides the internal state, and for some datastructures this might be important. (Such as Effect
).
Others expose other functions or data constructors with which you _can_ access the internal state if you want to. (Like Ok
and Err
in Result
, or many of the functions in the List
module)
But you can only really call those from outside the callback chain, not from the inside
i.e. you cannot call them on the thing you are working on in the callback chain, because there you don't have a Result a err
or a List a
, you only have an a
.
Yes! That makes sense.
Thank you for this 101 course :D I needed it
You're very welcome! :blush:
here's an interesting progression of moving from non-backpassing to backpassing style: all 4 of these compile to exactly the same thing
indented
result =
fields = Str.split line "|"
Result.try (List.get fields 0 |> Result.map exclaim) \title ->
Result.try (List.get fields 1 |> Result.try Str.toU16) \year ->
Result.try (List.get fields 2) \cast ->
Ok { title, year, cast: Str.split cast "," }
outdented
result =
fields = Str.split line "|"
Result.try (List.get fields 0 |> Result.map exclaim) \title ->
Result.try (List.get fields 1 |> Result.try Str.toU16) \year ->
Result.try (List.get fields 2) \cast ->
Ok { title, year, cast: Str.split cast "," }
backpassing
result =
fields = Str.split line "|"
title <- Result.try (List.get fields 0 |> Result.map exclaim)
year <- Result.try (List.get fields 1 |> Result.try Str.toU16)
cast <- Result.try (List.get fields 2)
Ok { title, year, cast: Str.split cast "," }
backpassing with |>
result =
fields = Str.split line "|"
title <- List.get fields 0 |> Result.map exclaim |> Result.try
year <- List.get fields 1 |> Result.try Str.toU16 |> Result.try
cast <- List.get fields 2 |> Result.try
Ok { title, year, cast: Str.split cast "," }
I don’t think this is a particularly good benchmark, but it is interesting to see how on my small phone only the last version is understandable without scrolling! C8C09E23-67E7-44B0-8EFD-EF3416673916.png DDEC1422-FC58-41E1-A2FF-D1AB097F1BF1.png
It isn't a good benchmark, but I think it does point out the truth of readability. I switched some code over to the last version recently because I find it way less noisy and easier to follow.
Of course if some of them were using different functions instead of Result.try
and that was an important detail, it may not be the best for readability anymore.
Last updated: Jul 05 2025 at 12:14 UTC