Stream: ideas

Topic: Backpassing ability


view this post on Zulip Tommy Graves (Jul 07 2022 at 16:09):

Would it be possible to create an ability to define a default behavior for backpassing for a type? Something like:

Backpassable is
  after : a, (b -> c) -> c | a implements Backpassable

  after : Result a err, (a -> Result b err) -> Result b err
  after = \result, transform ->
      when result is
          Ok v ->
              transform v
          Err e ->
              Err e

Now instead of:

x <- Result.after (somethingReturningResult _)
doSomethingWith x

You can do:

x <- somethingReturningResult _
doSomethingWith x

And you can do the same sort of thing with Effect.after, Task.await, etc. as well. (Result, Effect, Task would all implement Backpassable)

This is similar to defining the bind operator for a type in Haskell but I'm unclear on if you need HKTs to make this work correctly, and also whether it is possible to make backpassing do something different when the value passed to it implements Backpassable (in which case a <- b would desugar into after b (\a -> ...)) versus when it doesn't (in which case a <- b desugars into b (\a -> ...)

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:19):

so the example wouldn't type-check without higher-kinded types

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:20):

Backpassable is
  after : a, (b -> c) -> c | a implements Backpassable

since b doesn't appear anywhere else, this is basically the same as:

Backpassable is
  after : a, (* -> c) -> c | a implements Backpassable

which says that the * -> c function needs to accept any type, with no restrictions

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:20):

so after : Result a err, (a -> Result b err) -> Result b err wouldn't satisfy that type, as written, because a is more restricted than *

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:24):

there are other ideas for ways to do this though

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:25):

for example, something like this:

chain Task.await
    str <- File.readUtf8 myPath
    response <- Http.postUtf8 myUrl str

    Task.succeed response

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:26):

where chain is a language keyword that automatically applies the given function to all the <-s

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:26):

or could also call it do :big_smile:

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:27):

a related idea is to have that syntax but also allow it at the top-level, which would apply it to the entire file

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 16:40):

for example, something like this:
Yeah, something to remove all of the after and always would be nice. I think it would help readability a lot. Though I guess if you know that you will always be using a function with after, you could put the after in the function and make it take a lambda. But that is less flexible.
This is a quite noisy:

inplace = \cpu, addressing, op ->
    ref <- byRef cpu addressing
    readByte <- after (readMem ref.addr)
    out <- after (op {cpu: ref.cpu, byte: readByte})
    _ <- after (writeMem ref.addr out.byte)
    always out.cpu

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:41):

yeah so like in that example, you could do:

inplace = \cpu, addressing, op -> chain Task.await
    ref <- byRef cpu addressing
    readByte <- readMem ref.addr
    out <- op {cpu: ref.cpu, byte: readByte}
    _ <- writeMem ref.addr out.byte
    Task.succeed out.cpu

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 16:42):

Yeah. I think that we be a very nice improvement.

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:42):

I'm skeptical that the always/succeed is worth removing though - for example, if you said chain Task.await Task.succeed you're still writing the Task.succeed once, just in a different place :big_smile:

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 16:43):

Yeah, it doesn't work with Task.succeed. It should be fine with Effect.always.

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 16:43):

But probably can't reasonably just pick one

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:44):

yeah I wouldn't want to couple it to a particular type, especially since 3-param Task can't be an alias for Effect (since type aliases can't have phantom types)

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 16:45):

The after/await is definitely the bigger when on noise reduction anyway.

view this post on Zulip Richard Feldman (Jul 07 2022 at 16:45):

for sure, yeah

view this post on Zulip Tommy Graves (Jul 07 2022 at 17:22):

How common will it be to want to use both Result.after and Task.await in the same function? With chain you'd have to pick one of them and then explicitly call the other, right? (BTW, I like chain over do for the name if only for the searchability improvement).

Also, I still think we should rename Result.after to Result.try like Gleam does. To me, after doesn't indicate anything about what it is going to do. I think try at least sort of indicates that it has something to do with Ok/Err.

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 17:24):

Result.after is used for non-effects. Task.await is for effects. So I think generally they won't be mixed, but I could be wrong.

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 17:28):

Also, you should be able to promote a result into a task and the use it with Task.await. that would probably be as clean as forcing a call to Result.after, but still less clean than your original proposal. Though more explicit.

view this post on Zulip Tommy Graves (Jul 07 2022 at 17:37):

Here's the sort of example I'm thinking of where you would mix them:

getComplexDataFromDatabase  = \query ->
  data <- Task.await (Database.executeQuery query)
  _ <- Result.after (verifyData data)
  doSomethingWith data

where verifyData is some function that inspects some properties of data and returns Ok data if those properties meet some expectation, and Err e if they don't. (E.g., imagine that the first line gives you a record representing a customer's account, and the second line verifies that the record indicates the account is active and ready to have some operation performed on it, which the third line then performs).

Basically, any case where you want to do a non-effectual check or transformation of data that could fail due to some properties of the data, and the data is the result of an effect.

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 17:46):

I guess with chaining that could be:

getComplexDataFromDatabase  = \query -> chain Task.await
  data <- Database.executeQuery query
  _ <- Task.fromResult (verifyData data)
  doSomethingWith data

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 17:46):

But yeah, I total see the case for both now.

view this post on Zulip Tommy Graves (Jul 07 2022 at 17:54):

I probably should have said this in the original post, but one of the main advantages of reducing or eliminating the need to call Task.await or Result.after is it gets rid of the parentheses. The unfortunate thing about the line

_ <- Task.fromResult (verifyData data)

(or the equivalent with Result.after) is that the most important part of the line is tucked away in parentheses, and I think we generally are trained to focus on things not wrapped in parentheses first.

Although I guess I saw someone suggesting that the convention be

_ <- verifyData data |> Task.fromResult

which does improve the problem quite a bit. That combined with chain for the function you are using the most with backpassing seems like it has much better readability. (Or maybe in practice you would only use chain if you only had one type of backpassing function you were using)

view this post on Zulip Ayaz Hafiz (Jul 08 2022 at 01:48):

Crazy idea: add a language-level way to define what it means for a type to "chain" something, which the "chain" keyword then de-sugars.

For example:

Result o e : [Ok o, Err e]

Result.try : Result a e -> (a -> Result b e) -> Result b e
chain Result a e -> Result b e over a with Result.try

Effect a := {} -> a
after : Effect a -> (a -> Effect b) -> Effect b

chain Effect a -> Effect b over a with after

and then

getComplexDataFromDatabase  = \query -> chain
  data <- Database.executeQuery query
  _ <- verifyData data
  doSomethingWith data

desugars to

getComplexDataFromDatabase  = \query ->
  data <- Effect.after (Database.executeQuery query)
  _ <- Result.try (verifyData data)
  doSomethingWith data

so a very very weak form of Haskell's do-like construct without a need for higher kinds and specialized for the common case of chained actions

view this post on Zulip Tommy Graves (Jul 08 2022 at 02:18):

You're speaking my language Ayaz, other than calling this idea crazy :stuck_out_tongue:

view this post on Zulip Brendan Hansknecht (Jul 08 2022 at 02:25):

How does this work with the case were you actually want to handle an error. Do you just lose backpassing completely?

view this post on Zulip Ayaz Hafiz (Jul 08 2022 at 02:35):

yeah, or you don't use the chain keyword at the top of the scope you're doing the backpacking in. Alternatively you have a syntax like <~ instead of <- for "backpacking with chain sugar"

view this post on Zulip Brendan Hansknecht (Jul 08 2022 at 02:40):

I thought with your example chaining would be default and automatic. So no chain keyword at the calling function? Just a chain definition

edit: I see it now. Still use the keyword, but no specific ying the function at the calling location

view this post on Zulip Brendan Hansknecht (Jul 08 2022 at 02:40):

Also multiple arrows make sense, but I bet it would really confuse newer users

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:48):

I just realized: this actually has a type mismatch :big_smile:

getComplexDataFromDatabase  = \query ->
  data <- Task.await (Database.executeQuery query)
  _ <- Result.after (verifyData data)
  doSomethingWith data

this would desugar to:

getComplexDataFromDatabase  = \query ->
    Task.await (Database.executeQuery query) \data ->
        Result.after (verifyData data) \_ -> dosomethingWith data

the type mismatch is that Task.await needs its callback to return a Task, but Result.after returns a Result (and also needs its callback to return a Result, so there would be a second type mismatch if dosomethingWith data returned a Task!)

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:49):

I had this feeling like "I'm pretty sure I don't see that pattern come up in Haskell do notation, but I should keep an open mind because maybe it's actually awesome and people are just missing out" but I think the reason I don't see it is that it wouldn't compile :laughing:

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:50):

all of which is to say: I don't think mixing Task and Result in backpassing can work - you have to convert the Result to a Task and have all the chaining be through Task.await

view this post on Zulip Tommy Graves (Jul 08 2022 at 02:51):

Oh :facepalm:. I wonder in practice how easy it would be to understand what's going on if you tried to do that. It would be sweet if the compiler recommend the incantation to convert it to a task if you used Result.after

view this post on Zulip Tommy Graves (Jul 08 2022 at 02:52):

That certainly makes the chain approach more appealing, though!

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:53):

we could pretty easily have a Task.fromResult : Result ok err -> Task ok err

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:53):

that just turns Ok into Task.succeed and Err into Task.fail

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:53):

so then you could do _ <- Result.after (verifyData data) |> Task.fromResult

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:54):

with the postfix |> Task.await style, this could be:

getComplexDataFromDatabase  = \query ->
    data <- Database.executeQuery query |> Task.await
    _ <- verifyData data |> Task.fromResult |> Task.await
    doSomethingWith data

view this post on Zulip Tommy Graves (Jul 08 2022 at 02:54):

You don't need Result.after there do you?

view this post on Zulip Richard Feldman (Jul 08 2022 at 02:55):

oops, right! Fixed. :big_smile:

view this post on Zulip Tommy Graves (Jul 08 2022 at 02:57):

You can also define a shorthand like Task.awaitFromResultif that comes up a lot so you only need to do one pipe instead of two, although that won't work as well if you introduce chain.

view this post on Zulip Ayaz Hafiz (Jul 08 2022 at 02:57):

regarding the error: here is what rust would have to say about it: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cb78204a6c3fe7f8feaf4f501bd9c965. I imagine we could do better, especially without the From indirection piece

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

I had this feeling like "I'm pretty sure I don't see that pattern come up in Haskell do notation, but I should keep an open mind

in Haskell, this is where you'd introduce a nested do-block. (Or move it to a separate function, for clarity).
Or alternatively, it is where "monad transformers" :robot: come in. This is essentially syntactic sugar around the "I want to work on Results but I need to return a Task, let me convert the input from Task to Result and the output from Result back to Task" problem.


Last updated: Jun 16 2026 at 16:19 UTC