Stream: beginners

Topic: Backpassing use cases


view this post on Zulip jan kili (Jul 21 2022 at 11:53):

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?

view this post on Zulip jan kili (Jul 21 2022 at 11:54):

It seems like a much more powerful (or FP fundamental) feature than I'd originally thought!

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:54):

it is, but that also makes it tricky to talk about in a clear way

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:55):

a decent intuition is that it is about the ordering of operations. first do this, then that, then that

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:56):

this is achieved with a data dependency: the x <- part of the backpassing relies on the value of the <- .... part being available

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:56):

so now think about "ordered" things: threading a state through a computation, updating a random seed, printing lines to the console in order

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:57):

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

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:58):

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

view this post on Zulip Folkert de Vries (Jul 21 2022 at 11:58):

(it must behave in a certain way, too, not any implementation will do)

view this post on Zulip jan kili (Jul 21 2022 at 12:20):

... is -> just monadic =?!

view this post on Zulip jan kili (Jul 21 2022 at 12:23):

I'm finally grasping monads and it looks like -> is more of their chaining operator than |>...

view this post on Zulip jan kili (Jul 21 2022 at 12:23):

Maybe more of a "raw" chaining operator?

view this post on Zulip jan kili (Jul 21 2022 at 12:39):

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 =?

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:39):

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

view this post on Zulip jan kili (Jul 21 2022 at 12:40):

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

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:43):

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.tryare three examples

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:44):

view this post on Zulip jan kili (Jul 21 2022 at 12:45):

Right, but the variable passed as the parameter isn't the right-hand evaluation itself, it's a variable within that evaluation's context.

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:45):

Correct

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:46):

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.

view this post on Zulip jan kili (Jul 21 2022 at 12:47):

:D somehow that helps me!

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:48):

:green_heart:

view this post on Zulip jan kili (Jul 21 2022 at 12:49):

Is it only useful in synchronous situations when the internal state is intended to be hidden from the outside?

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:49):

Hooray! 🥳

view this post on Zulip jan kili (Jul 21 2022 at 12:50):

Otherwise we could just use = and pass that internal state around in a more complex return value.

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:50):

It is useful in any situation where it makes sense to keep track of something extra 'to the side' of plain sequencing.

view this post on Zulip jan kili (Jul 21 2022 at 12:51):

...without that side thing being visible/accessible in the calling context?

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:51):

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.

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:54):

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

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:55):

:point_of_information: this is actually what is going on behind the scenes in the RNG example

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 12:57):

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.

view this post on Zulip jan kili (Jul 21 2022 at 12:57):

Yes! This reminds me of my RNG example - jinx! You beat me to it haha

view this post on Zulip jan kili (Jul 21 2022 at 12:59):

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?

view this post on Zulip jan kili (Jul 21 2022 at 13:01):

For example, in the above snippet, if we switched to using backpassing for any of those foo/bar calls, can myfun return any states at all or just vals?

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 13:04):

It could only access vals

view this post on Zulip jan kili (Jul 21 2022 at 13:06):

Okay, thank you. That answers a question I couldn't formulate until now.

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 13:06):

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)

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 13:07):

But you can only really call those from outside the callback chain, not from the inside

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 13:08):

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.

view this post on Zulip jan kili (Jul 21 2022 at 14:42):

Yes! That makes sense.

view this post on Zulip jan kili (Jul 21 2022 at 14:42):

Thank you for this 101 course :D I needed it

view this post on Zulip Qqwy / Marten (Jul 21 2022 at 14:53):

You're very welcome! :blush:

view this post on Zulip Richard Feldman (Jul 23 2022 at 16:21):

here's an interesting progression of moving from non-backpassing to backpassing style: all 4 of these compile to exactly the same thing

view this post on Zulip Richard Feldman (Jul 23 2022 at 16:22):

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 "," }

view this post on Zulip Richard Feldman (Jul 23 2022 at 16:22):

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 "," }

view this post on Zulip Richard Feldman (Jul 23 2022 at 16:22):

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 "," }

view this post on Zulip Richard Feldman (Jul 23 2022 at 16:28):

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 "," }

view this post on Zulip Tommy Graves (Jul 23 2022 at 16:33):

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

view this post on Zulip Brendan Hansknecht (Jul 23 2022 at 16:56):

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