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 -> ...)
so the example wouldn't type-check without higher-kinded types
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
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 *
there are other ideas for ways to do this though
for example, something like this:
chain Task.await
str <- File.readUtf8 myPath
response <- Http.postUtf8 myUrl str
Task.succeed response
where chain is a language keyword that automatically applies the given function to all the <-s
or could also call it do :big_smile:
a related idea is to have that syntax but also allow it at the top-level, which would apply it to the entire file
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
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
Yeah. I think that we be a very nice improvement.
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:
Yeah, it doesn't work with Task.succeed. It should be fine with Effect.always.
But probably can't reasonably just pick one
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)
The after/await is definitely the bigger when on noise reduction anyway.
for sure, yeah
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.
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.
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.
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.
I guess with chaining that could be:
getComplexDataFromDatabase = \query -> chain Task.await
data <- Database.executeQuery query
_ <- Task.fromResult (verifyData data)
doSomethingWith data
But yeah, I total see the case for both now.
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)
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
You're speaking my language Ayaz, other than calling this idea crazy :stuck_out_tongue:
How does this work with the case were you actually want to handle an error. Do you just lose backpassing completely?
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"
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
Also multiple arrows make sense, but I bet it would really confuse newer users
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!)
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:
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
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
That certainly makes the chain approach more appealing, though!
we could pretty easily have a Task.fromResult : Result ok err -> Task ok err
that just turns Ok into Task.succeed and Err into Task.fail
so then you could do _ <- Result.after (verifyData data) |> Task.fromResult
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
You don't need Result.after there do you?
oops, right! Fixed. :big_smile:
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.
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
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