Understanding Rust Closures and the Fn Traits Hierarchy

Rust closures—anonymous, self-contained function expressions—are central to functional patterns and API design in Rust. Their behavior is governed by three core traits: FnOnce, FnMut, and Fn. These are not arbitrary distinctions but precise compile-time contracts tied to ownership, mutability, and call semantics.

Core Trait Differences

The following table summarizes how each trait governs closure usage:

Trait Call Count Mutable Access to Captured Values Ownership Transfer of Captures Typical Use Case
FnOnce Exactly once Yes (if moved) Yes — consumes captured values One-shot callbacks, e.g., thread startup logic
FnMut Multiple times Yes — via mutable borrow No — borrows mutably Stateful iterators, accumulators, event handlers with internal state
Fn Multiple times No — only immutable borrow No — borrows immutably Pure transformations, mapping, filtering, shared callbacks

Key clarifications:

  • Ownership transfer applies only to captured variables—not function arguments.
  • The move keyword forces copying or moving captured values into the closure’s environment, regardless of trait. It does not dictate which traits implemented—it affects how captures are stored, not what interface they expose.
  • A closure that mutates a captured variable cannot implement Fn, because Fn requires immutable access. It will always satisfy at least FnMut (and therefore FnOnce).
  • Closures automatically implement all applicable traits. For example, an immutable-capturing closure implements all three; a mutating one implements FnOnce and FnMut, but never Fn.

Capture Semantics Explained

A variable is captured when it is used inside a closure but declared outside its scope:

let title = String::from("Quantum Algorithms");
let printer = || println!("Book: {}", title); // `title` is captured
printer();

Capture mode inference:

  • No move, no mutation → Captures by immutable reference → Implements Fn, FnMut, FnOnce.
  • No move, but mutation → Captures by mutable reference → Implements FnMut and FnOnce (but not Fn).
  • With move, no mutation → Takes ownership of captures → Implements FnOnce; may also implement Fn or FnMut if the owned types support borrowing (e.g., String allows immutable/mutable borrows after move).
  • With move, and mutation → Owns and mutates → Implements FnOnce; FnMut possible if type permits mutable dereferencing (e.g., Box<T> or Vec<T>).

Determining Implemented Traits Programmatically

You can verify trait conformance using generic functions constrained by each trait:

fn accepts_once<F: FnOnce()>(f: F) { f(); }
fn accepts_mut<F: FnMut()>(mut f: F) { f(); f(); }
fn accepts_fn<F: Fn()>(f: F) { f(); f(); }

fn main() {
    let data = String::from("hello");
    
    // Immutable capture → satisfies all three
    let c1 = || println!("{}", data);
    accepts_once(c1.clone()); // OK
    accepts_mut(c1.clone());  // OK
    accepts_fn(c1);           // OK

    let mut counter = 0;
    // Mutable capture → satisfies FnOnce & FnMut only
    let c2 = || { counter += 1; };
    accepts_once(c2.clone()); // OK
    accepts_mut(c2);          // OK
    // accepts_fn(c2);        // ❌ Compile error
}

Practical Usage Patterns

These traits enable flexible, type-safe abstractions:

  • Callback registration: APIs accept Box<dyn Fn()> for reusable listeners.
  • Iterator adaptors: map() expects F: Fn(&T) -> U, while for_each() uses F: FnMut(&T).
  • Thread spawning: std::thread::spawn requires FnOnce + Send because the closure runs exactly once in another thread.

Real-World Example: Stateful Book Processor

#[derive(Debug, Clone)]
struct Publication {
    name: String,
    year: u16,
}

impl Publication {
    fn new(name: &str, year: u16) -> Self {
        Publication { name: name.into(), year }
    }
}

fn main() {
    let mut catalog = vec![
        Publication::new("Rust in Action", 2021),
        Publication::new("The Rust Programming Language", 2018),
    ];

    // FnMut: modifies external vector
    let mut adder = || catalog.push(Publication::new("Zero To Production", 2022));
    adder();
    
    // Fn: safe to share across threads or reuse
    let formatter = || catalog.iter().for_each(|p| println!("{} ({})", p.name, p.year));
    formatter();
    formatter(); // ✅ Works twice

    // FnOnce: consumes a value
    let consumer = move || {
        let first = catalog.remove(0);
        println!("Consumed: {}", first.name);
    };
    consumer();
    // consumer(); // ❌ Not allowed
}

Tags: rust closures fn-trait ownership moving-semantics

Posted on Sun, 24 May 2026 17:27:54 +0000 by mxicoders