Live data from Hacker News

Understanding Rust Closures

antoine.vandecreme.net

31–37 of 37 posts

Re: Understanding Rust Closures

#31

Earlier quoted context omitted.

Fn can only be called concurrently if its environment is Sync, which is often true but not necessarily. 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.

I'm not sure myself, does Fn correspond to reentrancy or is there some detail I am missing?

`Fn`, `FnMut`, and `FnOnce` can also implement and not implement `Sync` (also `Send`, `Clone`, `Copy`, lifetime bounds, and I think `use` applies to `impl Fn...` return types).

EDIT: https://news.ycombinator.com/item?id=46750011 also mentioned `AsyncFn`, `AsyncFnMut`, and `AsyncFnOnce`.

Re: Understanding Rust Closures

#32
post #2

Closures are the bread and butter of functional programming, but Rust made closures a complicated mess.

Closures are a complicated mess. Functional programming languages hide the mess with garbage collection.

Machine code and LLVM are complicated messes. Higher-level language hide a lot, but sometimes issues pop up, even in Rust e.g. inline heuristics (https://nnethercote.github.io/perf-book/inlining.html).

Re: Understanding Rust Closures

#33
post #3
post #2

Closures are the bread and butter of functional programming, but Rust made closures a complicated mess.

If you understand the borrow checker, closures are just not that much on top of things. In fact I can’t remember the last time I had to fight with them.

Closures are pretty simple in relation to their captures lifetimes, but they do have a lot of complexity in how the lifetimes of their argument and return type are computed. The compiler has to basically infer them, and that can easily go very wrong. The only reason it works most of the time is because closures are immediately passed to functions whose trait bound specify the expected signature of the closure, but once you deviate a little bit from the common case things start to break down. For example if the bound is `F: SomeTrait` where `SomeTrait` is implemented for `FnOnce(&' i32) -> &i32` the inference will break. Similarly if you store the closure in a local variable before passing it to the function. This used to come up pretty often for "async" closures that were supposed to take a reference as input, since it's impossible to specify their correct trait bound using directly the `Fn*` traits. There are a bunch of related issues [1] in the rustc repo if you search for closure and higher ranked lifetimes.

[1]: https://github.com/rust-lang/rust/issues?q=is%3Aopen%20is%3A...

Re: Understanding Rust Closures

#34
post #2

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 ...>` 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.

Re: Understanding Rust Closures

#35
post #16

Earlier quoted context omitted.

It isn't really type inference. Each closure gets a unique type. Rather it's an automatic decision of what traits (think roughly "superclasses" I guess if you aren't familiar with traits/typeclasses) to implement for that type.

So you're saying... it's type inference of type classes, just like in Haskell?

No, I don't think so, not unless there's some feature of Haskell type classes I'm completely unaware of.

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).

Re: Understanding Rust Closures

#36
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.

Re: Understanding Rust Closures

#37

Earlier quoted context omitted.

You most definitely can. fn foo (f: F) where F: Fn(T), T: ToString, { f("Hello World") } Or did I not understand what you meant?

Something like this isn’t possible fn foo(f: impl for 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() {…

Ha, yes, I see what you mean now. That's not really the closure's fault but monomorphization of the foo function. The specific thing you want to do would require boxing the value, or do more involved typing.
Post reply on HN