9 comments

[ 4.2 ms ] story [ 20.3 ms ] thread
Closures are the bread and butter of functional programming, but Rust made closures a complicated mess.
Functional programming languages usually don't support linear/affine types, non-gc references and mutations.

Their closures are essentially the equivalent of Rust's `Rc<dyn Fn(...) -> ...>` with some sugar for expressing the function type and hiding all the `.clone()`s needed.

It's easy to get simplier results if you support less features and use cases.

if I'm not mistaken (and I very well may be!) my primary confusion with closures comes from the fact that: the trait they implement (FnOnce / Fn / FnMut) depends entirely upon what happens inside the closure.

It will automatically implement the most general, relaxed version (FnMut I think?) and only restrict itself further to FnOnce and Fn based on what you do inside the closure.

So, it can be tricky to know what's going on, and making a code change can change the contract of the closure and therefore where and how it can be used.

(I invite rust experts to correct me if any of the above is mistaken - I always forget the order of precedence for FnOnce/Fn/FnMut and which implies which)

My issue is that there is no easy way to know the signature generated by the closure (unlike a function) until you read the compiler errors and even then it's some cryptic mess because closures are anonymous structs. Or maybe I missed some LSP config/extension?
And now we have the Async version of each of those, which makes me very happy and reduces the shenanigans and lifetime issues.

Also worthy of note is that there is talk to add a syntax for explicit captures.

(comment deleted)
I wish there was more customizability with regards to captures.

the move keywords captures everything. Sometimes I want a little bit more flexibility, like C++ lambdas.

The biggest friction I experience with respect to rust closures is their inability to be generic: I cannot implement a method that takes a closure generic over its argument(s).

So then I'm forced to define a trait for the function, define a struct (the closure) to store the references I want to close over, choose the mutability and lifetimes, instantiate it manually and pass that. Then the implementation of the method (that may only be a few lines) is not located inline so readability may suffer.

As a side note, there is a libffi wrapper on Rust that is exactly leveraging this code and data separation of closures in Rust: https://docs.rs/libffi

I've been using this on my vibewasm project to provide host function conversion to keep a C callable function in the front surface but doing my own custom wasm calling convention while capturing a persistent context pointer to the wasm store.

There is a side effect though: it is essentially unsound as you have to leak the object to the libffi closure which is a form of JIT -- dynamic code generation, it is, meaning Rust will have no way of knowing the lifetime of the pointer, or you have to always keep the libffi closure alive, meaning it is a permanent leak. I tried mitigate this by storing the closure in the wasm store, and we use that as the ultimate closure lifetime by designation -- any libffi function callback post store destruction is undefined behavior though.