Remix.run Logo
Understanding Rust Closures(antoine.vandecreme.net)
35 points by avandecreme 7 hours ago | 12 comments
andy_xor_andrew 3 hours ago | parent | next [-]

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)

yoshuaw an hour ago | parent | next [-]

> I always forget the order of precedence for FnOnce/Fn/FnMut

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`.
umanwizard an hour ago | parent [-]

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.

KolmogorovComp an hour ago | parent | prev | next [-]

This is correct. But it’s not really surprising, it’s type inference.

umanwizard an hour ago | parent | prev [-]

The least restrictive for the caller is Fn (you can call it whenever), then FnMut (you can call it only if you have exclusive access to it, as many times as you want), then FnOnce (you can call it only if you have exclusive owned access, and calling it once destroys it).

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.

amelius 3 hours ago | parent | prev [-]

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

openuntil3am 2 hours ago | parent | next [-]

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

Klonoar 3 hours ago | parent | prev | next [-]

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.

convolvatron 2 hours ago | parent [-]

I really wanted just yesterday to create a dyn AsyncFnMut, which apparently still needs async-trait to build the stable. but I was pretty much unable to figure out how to make that work with a lambda. saying this is all trivial once you understand the borrow machinery is really understating it.

kibwen an hour ago | parent | next [-]

> saying this is all trivial

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.

speed_spread 15 minutes ago | parent | prev [-]

Async is the stuff that messes up everything. Closures are not complicated.

ordu an hour ago | parent | prev [-]

Well... Rust is not a functional language, so it is not surprising that its closures are complicated.