Stream: ideas

Topic: Pipelined backpassing?


view this post on Zulip Brendan Hansknecht (Nov 11 2021 at 23:15):

This is another youtube inspired question. I have 0 idea if there is a reasonable way to do it, but it was interesting none the less.

Base example:

name <- await (File.read "username.txt")
data <- await (Http.get name)
File.write "response.txt" data

Version in youtube comment:

File.write "response.txt" <- await (Http.get <- await (File.read "username.txt"))

Obviously the youtube comment is not valid, but I was wondering if there would be any merit in trying to make a form of pipelining that works with backpassing. Like ultimately I want to write:

File.read "username.txt"
    |> Http.get
    |> File.write "response.txt"

This just doesn't work because of needing to await. Backpassing makes a nice version of this, but it requires explicitly naming of every intermediate value. Is there any way/should we add a way to pipeline functions that would normally require backpassing? Is this even really possible? Any thoughts?

view this post on Zulip Folkert de Vries (Nov 11 2021 at 23:16):

haskell uses the >>= as the infix version of await

view this post on Zulip Sebastian Fischer (Nov 21 2021 at 17:50):

Can we use await in combination with the pipe operator to hide names for intermediate values, at least when chaining single argument functions?

File.read "username.txt"
    |> await Http.get
    |> await (\d -> File.write "response.txt" d)

(I may be missing something. Still need to setup Roc.)

view this post on Zulip Brendan Hansknecht (Nov 21 2021 at 18:11):

Currently no, The last argument to await is a lambda. So the pipe wouldn't make sense. There is no value to pipe.

view this post on Zulip Sebastian Fischer (Nov 21 2021 at 19:41):

I'm on thin ice talking about code I did not try.

However, if I understand correctly, the task will be piped to await as first argument. The result of await is another task piped to the next await as first argument.

I did not write a lambda for Http.get because it is already a one argument function, that (I believe) can be passed to await as second argument (as it is in my example, because the pipe operator fills in the first argument.)

Am I missing something?

view this post on Zulip Brendan Hansknecht (Nov 21 2021 at 20:02):

I have to think about that more and test, maybe this is possible.

view this post on Zulip Lucas Rosa (Nov 21 2021 at 21:42):

what is await?

view this post on Zulip Lucas Rosa (Nov 21 2021 at 21:43):

and Task.await usually returns some Effect that needs to be matched on for Ok or Err

view this post on Zulip Lawrence Job (Nov 21 2021 at 21:44):

I was awaiting this question - are there docs/guides for how language mechanics like these so newcomers like myself can learn about the intended behaviour? I'm piecing stuff together from docs, the 5 videos and just experimenting so far

I don't mind writing the docs; I just don't know where to start

view this post on Zulip Brendan Hansknecht (Nov 21 2021 at 21:46):

await is Task.await, just depends how you import it.

view this post on Zulip Lucas Rosa (Nov 21 2021 at 21:48):

fair enough, there are some situations where you can pipe a Task but then other situations where you want to use the reverse lambda

view this post on Zulip Richard Feldman (Nov 21 2021 at 21:50):

I'm working on a language tutorial at the moment!

view this post on Zulip Sebastian Fischer (Nov 21 2021 at 21:50):

The signature depends on the platform that provides Task.await. I assumed the signature is like shown here:

https://github.com/rtfeldman/roc/blob/trunk/roc-for-elm-programmers.md

I agree that just because we can omit names for intermediate values when chaining one argument functions, that does not mean we should. I think it's an interesting question when to use which syntax and why. To answer that, it helps to be aware of all the options.

view this post on Zulip Brendan Hansknecht (Nov 21 2021 at 21:52):

Currently all platforms use a standard definition of Task. it probably will be a common and shared module in general

view this post on Zulip Brendan Hansknecht (Nov 21 2021 at 22:30):

So I looked deeper into the await with pipelining example. I think this should work:

Stdout.line "What's your first name?"
    |> await Stdin.line
    |> await (\firstName -> Stdout.line "Hi, \(firstName)!")

It doesn't currently, but I think that it should.
The big issue with it is that a value is only available to the next Task and not farther down the line. ex:

Stdout.line "What's your first name?"
    |> await Stdin.line
    # Note how firstname would need to be consumed here but we don't want to use it yet.
    |> await (\_firstName -> Stdout.line "What's your last name?")
    |> await Stdin.line
    # firstName would not be defined for this line.
    |> (\lastName -> Stdout.line "Hi, \(firstName) \(lastName)!")

view this post on Zulip Brendan Hansknecht (Nov 21 2021 at 22:35):

Figured out the correct syntax for the first example:

Stdout.line "What's your first name?"
    |> await (\{} -> Stdin.line)
    |> await (\firstName -> Stdout.line "Hi, \(firstName)")

Still has the same issue with not being able to use values later on in the function. Also, you still have to name things due to the lambda

view this post on Zulip Sebastian Fischer (Nov 22 2021 at 06:10):

Not being able to use intermediate values later down the pipeline when hiding their names is typical for the pipeline operator and not related to using it with tasks. If we need the name later we should not hide it - then we can use backpassing (or normal definitions when not using callbacks.)

When using pipelines with tasks, an explicit lambda is necessary on a callback only if that callback is not already a function with the correct type. So names can only be omitted in cases like above with Http.get. In practice, I would probably use backpassing instead of pipelines with explicit lambdas as callbacks.

In Elm, pipelines with tasks (where await is called andThen) are more ergonomic. Because of currying, explicit lambdas can be avoided more often in Elm. But I feel backpassing provides a good alternative in Roc, enabling us to use named intermediate results later.

I like how backpassing corresponds to the alternative of having interspersed definitions with explicit names when programming without callbacks (as shown in the doc I linked above.)


Last updated: Jun 16 2026 at 16:19 UTC