Closures and Fn Traits

Iterators Over Loops leaned on closures constantly, .map(|x| x * 2), .filter(|x| x.is_valid()), without explaining what a closure actually is under the hood. Every closure compiles down to an anonymous struct holding its captured variables, plus an implementation of one or more of three traits: FnOnce, FnMut, and Fn. Which trait a closure implements is determined entirely by how it uses what it captured, and that, in turn, decides where you're allowed to use it.


Capturing the Environment

A closure captures variables from its surrounding scope automatically, choosing the least restrictive mode it can get away with: by shared reference, by mutable reference, or by value (move).

let name = String::from("Ferris");

let greet = || println!("hello, {name}");  // captures `name` by reference
greet();
greet();
println!("{name}");  // still usable, only a reference was captured
let mut count = 0;

let mut increment = || count += 1;  // captures `count` by mutable reference
increment();
increment();
// count can't be used here while `increment` is still alive,
// it holds a mutable borrow

The move keyword forces capture by value, transferring ownership into the closure regardless of whether a reference would have sufficed:

let name = String::from("Ferris");

let greet = move || println!("hello, {name}");
// `name` is no longer usable here, the closure owns it now

move is mandatory whenever the closure needs to outlive the scope it was created in, most commonly when handing it to thread::spawn (see Concurrency in Practice), since the new thread might run after the original function has returned.


FnOnce, FnMut, Fn: A Hierarchy, Not a Choice

Every closure implements FnOnce, that's the trait for "can be called, consuming itself in the process." Whether it also implements FnMut and Fn depends on what calling it does to its captures:

  • FnOnce: callable once. Implemented by every closure. A closure that moves a captured value out of itself (e.g. move || some_string returning the owned String) can only be FnOnce, calling it again would try to move the same value twice.
  • FnMut: callable repeatedly, and may mutate its captures between calls. Implemented by closures that mutate what they captured, like the increment example above.
  • Fn: callable repeatedly through a shared reference, no mutation of captures at all. Implemented by closures that only read their captures.
fn call_once<F: FnOnce() -> String>(f: F) -> String {
    f()
}

fn call_many<F: FnMut()>(mut f: F) {
    f();
    f();
    f();
}

fn call_many_shared<F: Fn()>(f: F) {
    f();
    f();
}

Every Fn closure is also FnMut, and every FnMut closure is also FnOnce, the traits form a hierarchy from most to least restrictive:

TraitCallsDoes to capturesBound it when
Fnmanyonly reads themyou call the closure repeatedly and it mutates nothing
FnMutmanymay mutate themyou call it repeatedly and it carries state across calls
FnOnceoncemay consume themyou call it at most once (the widest set of closures)

A function that only needs to call its closure once should bound by FnOnce, that accepts the widest range of closures (including ones that move values out). Bounding by Fn when FnOnce would do needlessly rejects valid closures.

Gotcha: the direction of "restrictive" is the reverse of what it looks like. FnOnce is the least demanding bound on the caller's closure (anything goes), so it's the most permissive parameter bound. Fn is the most demanding requirement on the closure, so it's the most restrictive parameter bound. Reach for the loosest bound your function body actually needs.


Closures as Parameters: Generics vs Trait Objects

There are two ways to accept "something callable" as a parameter, the same generics-vs-dyn choice covered in Trait Objects vs Generics applies here too.

Generic parameter, monomorphized per closure type, zero runtime cost:

fn apply<F: Fn(i32) -> i32>(value: i32, f: F) -> i32 {
    f(value)
}

let doubled = apply(5, |x| x * 2);

Trait object, one compiled function, a small indirection cost, useful when you need to store closures of different origins in the same place:

struct EventHandler {
    callback: Box<dyn Fn(&str)>,
}

let handlers: Vec<Box<dyn Fn(&str)>> = vec![
    Box::new(|msg| println!("logger: {msg}")),
    Box::new(|msg| println!("alert: {msg}")),
];

for handler in &handlers {
    handler("something happened");
}

Default to the generic form for function parameters, it's both faster and the more idiomatic choice. Reach for Box<dyn Fn(...)> when the closures need to be stored in a struct or collection alongside others of varying origin, where a single concrete generic type isn't an option.


Returning Closures

A closure's type is anonymous, the compiler generates it, you can't name it. Returning one means returning impl Trait or boxing it:

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

let add_five = make_adder(5);
println!("{}", add_five(10));  // 15

move is required here: without it, the closure would try to borrow n, a local variable that's about to go out of scope when make_adder returns. impl Fn(i32) -> i32 works as a return type because every call site only ever sees one concrete closure type, the compiler knows exactly which one at the call site.

When the concrete type isn't knowable at compile time, returning different closures from different branches, for instance, impl Trait won't work and you need Box<dyn Fn(i32) -> i32> instead:

fn make_op(add: bool) -> Box<dyn Fn(i32) -> i32> {
    if add {
        Box::new(|x| x + 1)
    } else {
        Box::new(|x| x - 1)
    }
}

Each branch above is a distinct anonymous type; impl Trait requires a single concrete return type, while Box<dyn Fn(...)> erases that difference behind a pointer.


FnMut in Iterator Chains

Most iterator adapters take FnMut, not Fn, specifically so the closure is allowed to carry state across elements.

let mut seen = std::collections::HashSet::new();

let unique: Vec<_> = vec![1, 2, 2, 3, 1, 4]
    .into_iter()
    .filter(|x| seen.insert(*x))  // mutates `seen` on every call
    .collect();

println!("{unique:?}");  // [1, 2, 3, 4]

filter needs FnMut because it calls the predicate once per element and the closure above mutates seen each time. If it only took Fn, this stateful filtering pattern, deduplication, running counts, sampling every Nth item, would be impossible without reaching for a Cell or RefCell to work around the restriction.


Quick Reference

SituationReach for
Take a closure, call it oncef: impl FnOnce(...) -> ...
Take a closure, call it many times, it holds statemut f: impl FnMut(...)
Take a closure, call it many times, no mutationf: impl Fn(...)
Closure must outlive its scope (threads, returns)add move
Return one closure-> impl Fn(...) -> ...
Return different closures per branch-> Box<dyn Fn(...) -> ...>
Store closures of varying origin togetherVec<Box<dyn Fn(...)>>

Key Takeaways

  • A closure captures by reference, mutable reference, or by value (move), the compiler picks the least restrictive option that the closure's body requires.
  • FnOnceFnMutFn: every closure is FnOnce, FnMut closures are also FnOnce, Fn closures are also both. Bound parameters by the least restrictive trait that does the job, FnOnce accepts the widest range of closures.
  • Use a generic F: Fn(...) parameter by default, zero cost and idiomatic. Use Box<dyn Fn(...)> when you need to store closures of differing origin in the same struct or collection.
  • Returning a closure needs impl Fn(...) -> ... (single concrete type per call site) or Box<dyn Fn(...) -> ...> (multiple possible types, erased behind a pointer). move is required whenever the closure outlives the captured locals.
  • Iterator adapters like filter and map take FnMut, not Fn, specifically to allow stateful closures across calls.