Closure Traits: `FnOnce`, `FnMut`, and `Fn`

I have just (again) manually rewritten the section about closure trait bounds, as below.

This topic might be not too important in real life, but for me it was difficult to understand, at least I was mostly confused by the explanation in the official book. AI typically tries to replicate the explanation style of the official book -- and when I ask for a check or improvement, it has the strong tendency to delete my own text. My personal feeling is that my explanation strategy is quite good, I might do some fine-tuning (delete the diagram?), but I think I will keep it. What made it difficult for me to understand is the fact that the hierarchy of the traits practically reverses from the definition of a closure to their use as function parameters. What do you think?

12.2 Closure Traits: FnOnce, FnMut, and Fn

How a closure interacts with its captured environment determines which of the three closure traits it implements: FnOnce, FnMut, and Fn. These traits dictate whether the closure consumes (takes ownership of), mutates (mutably borrows), or only reads (immutably borrows) its environment.

Implementation Hierarchy:

The traits form an implementation hierarchy based on the closure's capabilities:

  • Every closure implements FnOnce, as any closure can conceptually be called at least once, potentially consuming its environment in the process.
  • Closures that do not consume their environment (they only borrow it mutably or immutably) also implement FnMut, allowing them to be called multiple times while potentially changing their environment.
  • Closures that only require immutable access to their environment (or capture nothing) also implement Fn, allowing them to be called multiple times without changing their environment.

This means Fn implies FnMut, and FnMut implies FnOnce. A closure implementing Fn can be used anywhere an FnMut or FnOnce is expected; an FnMut can be used where an FnOnce is expected.

+--------------------+     +---------------------+      +--------------------+
| Implements Fn      | --> | Implements FnMut    |  --> | Implements FnOnce  |
| (Immutable Borrow) |     | (& Immutable Borrow)|      | (& Mutable Borrow) |
| (Callable Many)    |     | (Callable Many)     |      | (& Consuming)      |
+--------------------+     +---------------------+      | (Callable Once)    |
                                                        +--------------------+

(Arrow indicates "also implements")

The compiler automatically determines the most specific trait(s) (Fn, FnMut, or just FnOnce) that a closure implements based on how its body interacts with captured variables.

Usage as Trait Bounds:

Functions accepting closures use these traits as bounds in their generic signatures (e.g., <F: FnMut(i32) -> i32>). When used this way, the hierarchy relates to the permissiveness of the bound – what kinds of closures the function accepts:

  1. F: FnOnce(...): This is the most permissive (least restrictive) bound. It accepts any closure matching the signature (Fn, FnMut, or FnOnce), as it only requires the closure to be callable at least once.
  2. F: FnMut(...): This bound is more restrictive. It accepts closures implementing FnMut or Fn (since Fn implies FnMut), requiring the closure to be callable multiple times, potentially mutating its environment. It rejects closures that only implement FnOnce (i.e., consuming closures).
  3. F: Fn(...): This is the most restrictive bound. It only accepts closures implementing Fn, requiring that the closure can be called multiple times without mutation. It rejects closures that only implement FnMut or FnOnce.

Choosing the right bound depends on how the function intends to use the closure: call once (FnOnce), call multiple times with mutation (FnMut), or call multiple times without mutation (Fn).

1 Like

I think when choosing between Fn and FnMut that it is less important whether mutation is intended to occur, and more important whether references to the function need to be exclusive. Fn allows calling the function through an & reference, which can be freely copied to allow multiple concurrent calls. FnMut only allows calling through an &mut reference, which can't be copied, so you can call it multiple times, but only through that one reference, so the calls have to be strictly sequential.

2 Likes

Yes, that is one more point which makes understanding these 3 traits a bit difficult. I think I will keep the basic text structure, but remove the diagram, as it provides no deeper insight. And I will see if I can find a few really good examples for these three traits. Maybe I could remove your cited sentence, as I think it is not really necessary.

I think your write up is pretty good. I also have some feedback. The feedback is about additional details you perhaps don't want to go into, if you're trying to keep things simple (i.e. they're not about something fundamental that was missed).

One important exception is that when a closure coded directly in a function argument position, and that argument is a generic annotated with a Fn* bound, the bound will drive inference. This includes both the closure signature, and the traits which the compiler will attempt to implement. It is important mostly in terms of the closure signature, but on rare occasion, being aware of this "inference override" is important for the traits implemented as well.

Another exception is that move will change how the environment is captured, by not performing any captures by reference.

Perhaps note: FnMut is more flexible than Fn for the caller, and the callee generally doesn't need Fn -- e.g. if you're taking the closure by value, calling it, and discarding it (which is typical). And FnOnce is even more flexible than FnMut for the caller. So when writing your own bounds, prefer FnOnce if you know you'll only ever call it once, and otherwise FnMut is more common than Fn.


And a side note not specific to the topic at hand:

I suggest using the terms "exclusive reference" and "shared reference" instead of "mutable reference" and "immutable reference" (everywhere, not just when talking about closure traits). That's more true to what they actually are, and IMO reduces learner confusion, e.g:

  • when shared mutation like Mutex or RefCell are inevitably encountered
  • when &mut related borrow-checker errors happen even though no actual mutation occurs
2 Likes

Thank you very much for your detailed explanations!

I suggest using the terms "exclusive reference" and "shared reference" instead of "mutable reference" and "immutable reference"

That is an interesting point. Unfortunately the Rust terminology is often a bit un-precise, and most authors seems to just copy the phrasing. I was always unhappy that when using plain stack variables like i32 many texts where talking about variable bindings, generating for some readers the impression that there are separate entities involved. "A borrowed reference" is also a term which we can sometimes find, when actually borrowed data is considered, and not the borrowing of a reference to some data. Or the often used term "compiler error" instead of the correct "compilation error".