a random thought I had this morning: with the | syntax for functions, we could try out keeping backpassing in a form like this: (an example from roc-pg)
query =
|orders| = from(Public.orders)
|customers| = join(Public.customers(on(.id, orders.customer_id)))
# ...
I'm not saying we should or shouldn't do this, just that:
Task in order to do any I/O at all. Purity inference has removed it from the learning curve, so the tradeoffs are different if it's only an advanced concept.a, b <- caused a lot of problems. I don't think |a, b| = would have the same problems, but @Joshua Warner would know better than I would. :big_smile: anyway, not saying we should do this or anything, just an interesting thought I figured I'd share! Curious what others think.
Ooh! Yeah, that gets around the parsing concerns.
Curious what the use cases would be now? Just “any time the body of a multi-line lambda takes up the rest of the scope?” Ie just avoiding some rightward drift?
I love this syntax. Don't know how often I would use it now that error handling is done differently. Would be better to try it now than after the 0.1 release. I think it's (more unfamiliar, but) better than Kotli's trailing labda syntax that people have suggested:
val product = items.fold(1) { acc, e -> acc * e }
So this would desugar to:
query =
from(Public.orders, |orders|
join(Public.customers(on(.id, orders.customer_id)), |customers|
# ...
)
)
and therefore just is useful in places where you might have a Pyramid of Doom? Is this truly more readable? Maybe it is, but I guess this is sugar that really hides some POTENTIAL performance penalties. And is that the kind of sugar we'd want in the language in a v1.0 of Roc? I don't really know
This is useful anywhere use is useful in Gleam, as it's the exact same feature
Which is generally for monadic APIs, context adding statements (time this block of code, or log its result), or defer alternatives (withOpen that gives a file handle and closes the file at the end of the block)
So it's probably the closest Roc can get to RAII, which I think is a good paradigm to be able to deploy
Personally, I would advise not trying this. Backpassing is essentially guaranteed to be a convenience at the cost of the weirdness budget. It also leads to designing more nested code (which isn't great).
If the community can live without backpassing, I think that is strictly a better state for roc to be in. If we can't, we will hit similar pain points to what led to backpassing in the first place.
We can always add it later. It is a convenience that I think hurts beginners and consistency the most. I think we also can look at pain points one at a time and likely come up with better solutions than backpassing for specific cases
-A former backpassing addict
Yeah, I'd need to see an RAII-like example. Their examples are a little silly:
import gleam/io
import gleam/result
pub fn main() {
let _ = io.debug(without_use())
let _ = io.debug(with_use())
}
pub fn without_use() {
result.try(get_username(), fn(username) {
result.try(get_password(), fn(password) {
result.map(log_in(username, password), fn(greeting) {
greeting <> ", " <> username
})
})
})
}
pub fn with_use() {
use username <- result.try(get_username())
use password <- result.try(get_password())
use greeting <- result.map(log_in(username, password))
greeting <> ", " <> username
}
// Here are some pretend functions for this example:
fn get_username() {
Ok("alice")
}
fn get_password() {
Ok("hunter2")
}
fn log_in(_username: String, _password: String) {
Ok("Welcome")
}
which in Roc (with PNC/SD) would be:
main = |_|
dbg(without_richards_idea())
dbg(with_richards_idea())
without_richards_idea = ||
get_username().map(|username|
get_password()map(|password|
log_in(username, password).map(|greeting|
"$(greeting),$(username)"
)
)
)
with_richards_idea = ||
|username| = get_username().map()
|password| = get_password().map()
|greeting| = log_in(username, password).map()
greeting <> ", " <> username
}
// Here are some pretend functions for this example:
get_username = ||
Ok("alice")
get_password = ||
Ok("hunter2")
log_in : String, String -> Result String _
log_in = |_username, _password|
Ok("Welcome")
Sam Mohr said:
So it's probably the closest Roc can get to RAII, which I think is a good paradigm to be able to deploy
We already have a platform level solution that fits roc even better and just depends on our refcounting to cleanup resources. Even when we had withOpen style apis and backpassing, they were rarely used due to the limitations of scope. Just calling open and letting the refcount close is a nicer API that sees far more use. I would rather see boxes with destructors in roc as a proper solution to RAII than to try and use scope based RAII with backpassing. Scope based is not enough.
https://erikarow.land/notes/using-use-gleam
Here's an article on use cases. Interestingly, the only one I think is relevant is the last case, "context management". We now have proper early returns, which we can use for managing results way better than with backpassing, and using use for unnesting the callback in List.map is just confusing.
I agree
And with for, and other error handling improvements, we'll see a lot less callbacks in general
I agree with @Brendan Hansknecht that having a destructor for Box is a cleaner solution for RAII. Generally I think Roc does well with a few, powerful primitives, but backpassing probably can be replaced by better, more numerous tools that don't lead to beginners needing to engage with a complex concept.
Yeah even the Result mapping case is better today with try or ?
with_question = ||
username = get_username()?
password = get_password()?
greeting = log_in(username, password)?
"$(greeting),$(username)"
with_try = ||
username = try get_username()
password = try get_password()
greeting = try log_in(username, password)
"$(greeting),$(username)"
Which is essentially the same
And if try desugars to when, probably better perf than callbacks
Adding another +1 for not adding this. I think it might be tricky to limit use of this feature to advanced use cases only, as it's super tempting to use it for designing APIs (I know at least one situation for a CI library where I'd be tempted myself).
The RAII that we had been doing before:
File := U64
withFile : Path, (File => Result {} err) => Result {} [OpenErr, ..err]
withFile = |path, useFile|
file = openFile!(path)?
res = useFile(file)
closeFile!(path)?
res
listUsers = ||
|userFile| = withFile("users.json")
Ok(usersFile.read!()?.decode()?.len())
File can't be created without withFile, so there's no way for it to escape our resource management
And now, since it's native, we can have a destructor for File right? to clean it up when refcount goes to 0?
With what I'd call refcounting BS, yes, but I think a "real" destructor solution would be preferable
I guess now you could do silly stuff like put it in a map or some other long-lasting data structure
Sam Mohr said:
With what I'd call refcounting BS, yes, but I think a "real" destructor solution would be preferable
What do you mean by "real" here?
Writing C++?
2 minutes
Sam is writing a treatise on RAII in functional languages right now :rofl:
Okay, so what I meant by "real" was that the File type is defined and primarily used from the high-level Roc side of things, and in my mind, the platform mainly handles passing that structural data to the OS in the way we expected. The platform having to manage refcounting for Roc concepts felt more to me like a requirement imposed by List refcounting than a tool introduced for its own sake to platform authors. Since Roc primarily owns File and primarily interacts with it, it would make more sense to me that we handle cleanup of File on the Roc side.
However, on reflection, I know we are doing lots of stuff on the host side with the owned data like read-only lists that manage the refcount of their RocList's accordingly, on top of the normal refcounting stuff. So I guess as long as this refcounting mechanism is considered a feature and not just an implementation detail, it makes sense to handle destructors from the host side.
In addition, the host is the only one that would really know what the actual resource is (in this case, a std::fs::File), so it makes sense for it to manage resources.
I wish I was haha, but I'm not an expert. It's more of an attempt to distinguish between the use of a feature for its intended purpose vs. hijacking that feature to achieve some different behavior. The feature in question here is refcounting and its exposure to the host instead of just &str or "a C struct with fields".
Just a note on Gleam as reference - the use thing really spread across usages that I wouldn't expect over there and now it is not uncommon to see it being used like:
fn sum(xs: List(Int)) -> Int {
use x, acc <- list.foldl(xs, 0)
acc + x
}
I think it is worth keeping that in mind as reference that when something is possible, people will use it in unplanned ways (tbh this is used like this by core folks on the gleam community so it would be taken as "best/common practice"?)
Georges Boris said:
Just a note on Gleam as reference - the
usething really spread across usages that I wouldn't expect over there and now it is not uncommon to see it being used like:fn sum(xs: List(Int)) -> Int { use x, acc <- list.foldl(xs, 0) acc + x }I think it is worth keeping that in mind as reference that when something is possible, people will use it in unplanned ways (tbh this is used like this by core folks on the gleam community so it would be taken as "best/common practice"?)
This is so true. If you give a mouse a cookie...
yeah backpassing used to be used in that way too sometimes. I didn't personally like that style, but it did see use! :big_smile:
I think an added convenience for monadic APIs would be nice.
I have an ocaml codebase that uses a reactive TUI library that lets you compose your UI using incremental computations.
Super nice way to build a UI. But you are constantly mapping and map2ing data when transforming say a reactive number into a reactive UI element showing that number.
That library exposes "let$" which essentially is backpassing but always calling a particular map function.
I don't think I'd even consider using it without the nice monadic syntax.
Otherwise the code would just be an incomprehensible mess of lambdas.
But yeah, I agree that we should wait till people notice meaningful suffering
One nice way I saw backpassing/use used in gleam was as a middleware API in the webserver library wisp. But there are other ways to get a similar API without backpassing. I really liked it when it was there, but I sadly think it is confusing for newcomers and hard to keep to advanced use cases only
@Eli Dowling could you link that to me here or in a DM or wherever? I'm constantly looking for ways to manage nested state for GUIs in functional languages
Sure
Here is the original project:
https://github.com/let-def/lwd
Here is my quite heavily modified fork:
https://github.com/faldor20/nottui
Here is the project I built with it:
https://github.com/faldor20/jj_tui
If you are curious about nottui. I wrote a really quick and dirty crash course:
https://github.com/faldor20/nottui/blob/main/tutorial%2Fhackernews%2Ftutorial.md
And I wrote a blog post going into how to think about writing nottui UIs too:
https://elidowling.com/blog/nottui_intro/
I'd like to get it into a more user friendly state one day... But you know... I'm busy :sweat_smile: so many side projects so little time
The GitHub version of my fork probably isn't totally up to date with the changes actually made in jj_tui. But it' all work. I'm just slowly getting it to be a bit more happy with Async executors and multi threading.
Thank you!
I'll warn you, whilst technically evaluating the incremental UI is pure I don't really worry at all about maintaining any kind of functional purity in the rest of the code.
Updating all the states is done by doing a couple checks and then writing to a ref.
I'll see what I can manage :wink:
Although thinking about it now... I could actually model all state updates as effects which might be really useful...
That would only work if Store guiState was allowed outside of tests, right?
Oh I meant Ocaml effects :sweat_smile:
Or if your platform provided a store_state!
But yeah if roc had arbitrary effects it would totally be possible.
There are just a couple of cases right now where you can deadlock the program by trying to set the program state while evaluating the UI. We have ways around it but they have their own confusing consequences.
It's not something you hit often but it's a rough edge.
Anyway let me know if you have any thoughts or questions if you check it out. I think it is an architecture that could fit well into the roc world too.
Will do! I've starred the message
I can't remember where I saw this, I think it was in a recent post for a Gleam release, but one reason why backpassing doesn't fit well in current Roc is a reason why Gleam can't have return, because it doesn't work well with use.
check_contents! = |path|
full_path = get_full_path(path)
|file| = with_file!(full_path)
if full_path.is_dir()
return "early"
contents = file.read!()?
...
That early return and the ? are going to return to with_file!, not to check_contents!
We could put a warning on early returns within backpassing contexts, but it can otherwise lead to some surprising behavior
Last updated: Jun 16 2026 at 16:19 UTC