Move in closures

In rust documentation it says

Note: move closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them

I can't understand why is it like that? when a closure moves a variable's ownership into it how can it implement Fn/FnMut?

Moving the captured value into the closure means you can only create that closure once (using that same value). But: you might be able to call it multiple times though. Closures that can be called multiple times will implement FnMut and sometimes also Fn.

If a closure's code moves the captured value out of the closure when its called, then (and only then) it can only be called once. For it to be able to move the value out of the closure when called, it must have been moved into the closure when it was created in the first place, hence the implication that if closure's code moves a captured value (when it's called), then the value is also captured by moving. It's only an implication though, not an equivalence: a closure's code might not move a captured value, but the value could still be captured by moving it.

The move keyword does exactly thst: it only influences the capturing part, but it's still fine that calling the closure won't move anything, and thus can happen multiple times.

The reason why capturing by value (i. e. by moving) can be useful (even if it wasn't necessary for executing its code) is because this eliminates borrows of local variables (and their severely limited lifetimes) from the closure's type, so you can return the closure from a function, or easily send it to another thread.

6 Likes

It may help to think of closures as a struct, one that the compiler defines implicitly; the fields of the struct are the captured variables. The move keyword determines how they are captured (by value or some sort of reference) -- what the types of the fields are. The compiler then implements the appropriate Fn traits for this struct.

It also helps to understand the relationship between the Fn traits:

  • Fn<_> takes &self, FnMut<_> takes &mut self, and FnOnce<_> takes self
    • These self receivers are the struct from above
  • Fn<Args> implies FnMut<Args> implies FnOnce<Args>, and they all have the same output type
    • Which is <_ as FnOnce<Args>>::Output
  • When the compiler implements these, you can think of it as implementing the most specific version possible, and the more general trait implementations defer to the more specific ones:
    • If Fn can be implemented, FnMut calls Fn (coercing &mut self to &self)
    • If FnMut can be implemented, FnOnce calls FnMut (taking a &mut to self)
  • If a type is known to implement multiple Fn traits, when you call it like a function, it calls the most specific trait that is implemented

Which trait is the most specific that can be implemented depends on what the closure function body does with the fields. If you move a non-copy field out, you can't implement FnMut or Fn -- you need self in order to move the field out, and once you do so, self is partially uninitialized and can't be used anymore. If you mutate any of your fields (captured variables), you can't implement Fn, because you need &mut self to do the mutation.

With this framework in mind, I find it easier to understand what's going on with closures and the Fn traits.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.