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.

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

Capture Examples:

  • Immutable Borrow (Fn): The closure only reads captured data.

    fn main() {
        let message = String::from("Hello");
        // Borrows 'message' immutably. Implements Fn, FnMut, FnOnce.
        let print_message = || println!("{}", message);
    
        print_message();
        print_message(); // Can call multiple times.
        println!("Original message still available: {}", message); // Still valid.
    }
  • Mutable Borrow (FnMut): The closure modifies captured data.

    fn main() {
        let mut count = 0;
        // Borrows 'count' mutably. Implements FnMut, FnOnce (but not Fn).
        let mut increment = || {
            count += 1;
            println!("Count is now: {}", count);
        };
    
        increment(); // count becomes 1
        increment(); // count becomes 2
        // The mutable borrow ends when 'increment' is no longer used.
        println!("Final count: {}", count); // Can access count again.
    }
  • Move (FnOnce): The closure takes ownership of captured data.

    fn main() {
        let data = vec![1, 2, 3];
        // 'drop(data)' consumes data, so closure must take ownership.
        // Implements FnOnce only.
        let consume_data = || {
            println!("Data length: {}", data.len());
            drop(data); // Moves ownership of 'data' into drop.
        };
    
        consume_data();
        // consume_data(); // Error: cannot call FnOnce closure twice.
        // println!("{:?}", data); // Error: 'data' was moved.
    }

12.2.1 The move Keyword

Use move before the parameter list (move || ...) to force a closure to take ownership of all captured variables. This is vital when a closure must outlive its creation scope, like in threads, ensuring it owns its data rather than holding potentially dangling references.

use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // 'move' forces the closure to take ownership of 'data'.
    let handle = thread::spawn(move || {
        // 'data' is owned by this closure now.
        println!("Data in thread (length {}): {:?}", data.len(), data);
        // 'data' is dropped when the closure finishes.
    });

    // println!("{:?}", data); // Error: 'data' was moved.

    handle.join().unwrap();
}

12.2.2 Closures as Function Parameters

Functions accepting closures use generic parameters with trait bounds (Fn, FnMut, FnOnce) to specify requirements.

// Accepts any closure that takes an i32, returns an i32,
// and can be called at least once.
fn apply<F>(value: i32, op: F) -> i32
where
    F: FnOnce(i32) -> i32, // Most general bound that allows calling once
{
    op(value)
}

// Accepts closures that can be called multiple times without mutation.
fn apply_repeatedly<F>(value: i32, op: F) -> i32
where
    F: Fn(i32) -> i32, // Requires only immutable borrow
{
    op(op(value)) // Call 'op' twice
}

fn main() {
    let double = |x| x * 2; // Implements Fn, FnMut, FnOnce
    println!("Apply once: {}", apply(5, double)); // Output: Apply once: 10
    println!("Apply twice: {}", apply_repeatedly(5, double)); // Outp: Apply twice: 20

    let data = vec![1];
    let consume_and_add = |x| { // Implements FnOnce only
        drop(data);
        x + 1
    };
    println!("Apply consuming closure: {}", apply(5, consume_and_add)); // Output: 6
    // apply_repeatedly(5, consume_and_add); // Error: 'Fn' bound not met
}

Choose the most restrictive bound needed: FnOnce if called once, FnMut if called multiple times with mutation, Fn if called multiple times without mutation.

12.2.3 Function Pointers vs. Closures

Regular functions (fn name(...)) implicitly implement Fn* traits if their signature matches. They can be passed where closures are expected, but cannot capture environment variables.

fn add_one(x: i32) -> i32 {
    x + 1
}

fn apply<F>(value: i32, op: F) -> i32
where
    F: FnOnce(i32) -> i32,
{
    op(value)
}

fn main() {
    let result = apply(10, add_one); // Pass the function 'add_one'
    println!("Result: {}", result); // Output: Result: 11
}