Understanding Rust Closures
Posted by avandecreme 5 days ago
Comments
Comment by andy_xor_andrew 5 days ago
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)
Comment by bobbylarrybobby 5 days ago
Comment by umanwizard 5 days ago
The least restrictive for the function itself is the opposite order: FnOnce (it can do anything to its environment, including possibly consuming things without putting them back into a consistent state), followed by FnMut (it has exclusive access to its environment, and so is allowed to mutate it, but not destroy it), followed by Fn (it has only shared access to its environment and therefore is not allowed to mutate it).
Since these orders are inverses of each other, functions that are easier to write are harder to call and vice versa. That’s why they implement the trait with the minimum amount of power possible, so that they can be called in more places.
Comment by yoshuaw 5 days ago
The way I remember the ordering is by thinking about the restrictions the various Fn traits provide from a caller's perspective:
1. FnOnce can only ever be called once and cannot be called concurrently. This is the most restrictive.
2. FnMut can be called multiple times but cannot be called concurrently. This is less restrictive than FnOnce.
3. Fn can be called multiple times and can even be called concurrently. This is the least restrictive.
So going from most to least restrictive gives you `FnMut: FnOnce` and `Fn: FnMut`.Comment by umanwizard 5 days ago
It’s more precise to say that Fn can be called even when you only have shared access to it, which is a necessary, but not sufficient, condition for being able to be called concurrently.
Comment by pwdisswordfishy 4 days ago
Comment by armchairhacker 4 days ago
EDIT: https://news.ycombinator.com/item?id=46750011 also mentioned `AsyncFn`, `AsyncFnMut`, and `AsyncFnOnce`.
Comment by csomar 4 days ago
Comment by krukah 5 days ago
FnOnce
FnMut
Fn
Comment by KolmogorovComp 5 days ago
Comment by gpm 5 days ago
Comment by chowells 4 days ago
Comment by gpm 4 days ago
If anything it's closer to SFINAE in C++ where it tries to implement methods but then doesn't consider it an error if it fails. Then infers type-classes based on the outcome of the SFINAE process. Or the macro analogy another poster made isn't bad (with the caveat that it's a type system aware macro - which at least in rust is strange).
Comment by csomar 4 days ago
Comment by stevefan1999 4 days ago
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.
Comment by BlackFly 4 days ago
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.
Comment by tuetuopay 4 days ago
fn foo<F, T>(f: F)
where
F: Fn(T),
T: ToString,
{
f("Hello World")
}
Or did I not understand what you meant?Comment by armchairhacker 4 days ago
fn foo(f: impl for<T: ToString> Fn(T)) {
f(“Hello World”);
f(3.14);
}
fn main() {
f(|x| println!("{}", x.to_string());
}
The workaround: trait FnToString {
fn call(&self, x: impl ToString);
}
fn foo(f: impl FnToString) {
f.call("Hello World");
f.call(3.14);
}
struct AnFnToString;
impl FnToString for AnFnToString {
fn call(&self, x: impl ToString) {
println!("{}", x.to_string());
}
}
fn main() {
foo(AnFnToString);
}Comment by tuetuopay 4 days ago
Comment by wtetzner 4 days ago
Comment by OptionOfT 4 days ago
the move keywords captures everything. Sometimes I want a little bit more flexibility, like C++ lambdas.
Comment by avandecreme 4 days ago
I agree it would be nice, in particular to make it easier to understand when learning the concept.
Comment by Sytten 5 days ago
Also worthy of note is that there is talk to add a syntax for explicit captures.
Comment by amelius 5 days ago
Comment by openuntil3am 5 days ago
Comment by andrewflnr 4 days ago
Comment by armchairhacker 4 days ago
Comment by Klonoar 5 days ago
In fact I can’t remember the last time I had to fight with them.
Comment by SkiFire13 4 days ago
[1]: https://github.com/rust-lang/rust/issues?q=is%3Aopen%20is%3A...
Comment by convolvatron 5 days ago
Comment by kibwen 5 days ago
The comment above isn't saying that closures are trivial. Once you understand the borrow checker, you understand that it's a miracle that closures in Rust can possibly work at all, given Rust's other dueling goals of being a GC-less language with guaranteed memory safety despite letting closures close over arbitrary references. Rust is in uncharted territory here, drawing the map as it goes.
Comment by speed_spread 5 days ago
Comment by SkiFire13 4 days ago
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.
Comment by ordu 5 days ago