Skip to content

← Closures and Iterators step 2 of 28

Medium Primitives

Fn, FnMut, FnOnce: the closure hierarchy

Three tiny generic helpers, one per rung of the closure ladder, and one function that uses all three.

pub fn each<F: Fn(i64) -> i64>(nums: &[i64], f: F) -> Vec<i64>
pub fn tally<F: FnMut(i64)>(nums: &[i64], f: F)
pub fn finish<F: FnOnce() -> String>(f: F) -> String

pub fn run_three(nums: Vec<i64>) -> (i64, Vec<i64>, String)

run_three must:

  1. call each with a closure that doubles, producing the second element of the tuple;
  2. call tally with a closure that adds each doubled number into a counter captured from the enclosing scope, producing the first element;
  3. call finish with a closure that moves a captured String out, producing the third element — exactly "n=<count> total=<total>", where <count> is how many numbers there were.

For [1, 2, 3] that is (12, [2, 4, 6], "n=3 total=12"). For [] it is (0, [], "n=0 total=0").

The ladder

There is no single “closure trait”. There are three, and they are nested:

Fn  ⊂  FnMut  ⊂  FnOnce

Read them as how many times can I call this, and what does calling it do to the captures:

Trait call receives Callable A closure gets it when
FnOnce self once always — every closure implements it
FnMut &mut self many, needs mut it does not move its captures out
Fn &self many, shared it neither moves nor mutates them

So the rule for what a given closure implements is mechanical:

  • Does it move a capture out of itself? Then it is FnOnce only.
  • Otherwise, does it mutate a capture? Then it is FnMut (and FnOnce).
  • Otherwise it is Fn (and FnMut, and FnOnce).

And the rule for what you should write in a bound is the mirror image: take the weakest bound that lets your body work. F: FnOnce() accepts strictly more closures than F: Fn(), because every Fn closure is also an FnOnce closure. A bound is a demand you make on your caller; demanding Fn when you only call once turns away perfectly good callers.

Reading someone else’s signature runs backwards, and this is the practical payoff: F: FnOnce tells you your closure will be called at most once, so it is safe to hand it something you are giving away. F: FnMut tells you it may be called many times and may accumulate state. F: Fn tells you it may be called many times, possibly from several places at once, and had better not depend on being called in any particular order.

The misconception this problem exists to kill

move makes a closure FnOnce.”

No. This closure is move, and it is Fn:

let x = 5;
let c = move || x + 1;
println!("{} {} {}", c(), c(), c());   // fine

move decides how captures get in (by value rather than by reference). The trait is decided by what the body does to them. move || x + 1 takes a copy of x and then only reads it, so it is Fn. What makes a closure FnOnce-only is moving a capture out — like the third helper here, whose closure returns a captured String by value. That String is gone after one call, so a second call cannot exist. Item 9.3 is about move on its own.

The starter does not compile — read both errors

It ships with two deliberate bugs, one per lesson.

In tally: E0596, cannot borrow \f` as mutable`. Calling an FnMut closure needs &mut self, so the binding holding it must be mutable. fn tally<F: FnMut(i64)>(nums: &[i64], f: F) does not give you that; mut f: F does. Note that mut on a parameter is not part of the function’s type — callers neither see it nor care. It is a property of the local binding only. You will meet this error a second time at the call site, on your own counter-mutating closure: a closure that mutates a capture is FnMut, and binding it to a non-mut let gives the same E0596. Two separate errors, same rule, and it is worth seeing both. In finish: E0382, use of moved value: \f``. The starter calls f() twice. FnOnce::call_once takes self by value, so the first call consumes f. There is no way to call an FnOnce twice; the type system is telling you the truth. Delete the second call.

Sequencing the borrows

Your counter closure holds a &mut to the counter for as long as the closure is alive. You cannot read the counter while that loan is live. If the compiler objects when you go to build the report, the fix is not RefCell — it is to let the closure die first, which usually means writing the tally call as its own statement and reading the counter afterwards. Item 9.27 is a whole problem about exactly this reordering.

Grade is compile + tests + clippy -D warnings.

Loading visualization…