17 — Closures
Closures are anonymous functions that capture their environment. They bridge the gap between functions and objects.
Syntax
let add = |a, b| a + b;
let square = |x: i32| x * x;
let greet = |name: &str| format!("hi {name}");
let block = |x| { let y = x + 1; y * 2 };
let no_args = || 42;
let void_closure = || { /* do something */ };
- Parameter types often inferred (especially when called immediately).
- Single expression OR
{ }block. - Can't have generic parameters (no
|<T> x| ...).
Capturing the Environment
Closures capture variables from the enclosing scope. The capture mode determines how:
let n = 5;
let add_n = |x| x + n; // borrows n by reference
let n_mut = String::from("a");
let consume = move || n_mut; // moves n_mut into closure
Capture Modes (Trait Hierarchy)
| Trait | How it captures |
|---|---|
FnOnce | May consume captures (move out) — callable once |
FnMut | May mutate captures — callable multiple times (mutably) |
Fn | Only immutable borrows — callable any number of times |
Each is a supertrait of the next: Fn: FnMut: FnOnce. So every Fn is also FnMut and FnOnce.
let mut s = String::from("hi");
let push_closure = || s.push('!'); // FnMut — needs mut capture
let consume_closure = || drop(s); // FnOnce — consumes s
move Keyword
Forces capture by value (move) regardless of how the closure uses them:
let s = String::from("hi");
let f = move || println!("{s}"); // s moved into f; original invalid
move is essential for thread spawning — closures sent to other threads must own their captures ('static).
Disjoint Closure Captures (Edition 2021)
let a = String::from("a");
let b = String::from("b");
let f = || println!("{a} {b}"); // edition 2018: borrows BOTH a and b
// edition 2021: borrows only what's used in each branch
The 2021 edition captures only the used fields of disjoint structs, enabling more code to compile.
Closures as Arguments
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn apply_mut<F: FnMut(i32) -> i32>(mut f: F, x: i32) -> i32 { f(x) }
fn apply_once<F: FnOnce() -> i32>(f: F) -> i32 { f() }
apply(|x| x + 1, 5);
Use impl Fn(...) for shorthand:
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) }
Returning Closures
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let add5 = make_adder(5);
add5(10); // 15
Use move so the closure owns its captures (otherwise it'd borrow a stack variable that's gone).
For multiple closure types in different branches, use Box<dyn Fn()>:
fn make(pred: bool) -> Box<dyn Fn(i32) -> i32> {
if pred { Box::new(move |x| x + 1) }
else { Box::new(move |x| x - 1) }
}
Closure Type is Unique
Each closure has an anonymous, unnameable type generated by the compiler. Two structurally identical closures have different types. This is why Box<dyn Fn()> exists for heterogeneous collections.
Closures Implement Traits
impl Fn(i32) -> i32 for SomeClosureType { ... }
Function pointers fn(A) -> B also implement Fn, so you can pass plain functions where impl Fn is expected.
Fn/FnMut/FnOnce Object Safety
let f: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + 1);
let fm: Box<dyn FnMut(i32) -> i32> = Box::new(|x| x + 1);
let fo: Box<dyn FnOnce(i32) -> i32> = Box::new(|x| x + 1);
Box<dyn FnOnce> is callable since Rust 1.35 (special support).
Closures and Iterators
Closures are the currency of iterator adapters:
v.iter().map(|x| x * 2).filter(|x| *x > 5).for_each(|x| println!("{x}"));
? in Closures
let parse = |s: &str| s.parse::<i32>()?; // closure can use ?
Closures can use ? if their return type supports it (Result or Option).
Recursion in Closures
Closures can't easily recurse — they don't have a name. Workarounds:
- Use a regular
fn. - Use a
Box<dyn Fn>and pass it to itself. - Use
Ycombinator (academic).
move Closure Captures and Lifetime
let s = String::from("hi");
let f = move || println!("{s}");
// s moved into f; f now owns the String
Without move, f would borrow s, which means s must outlive f. With move, f owns its captures and can be 'static.
Capture By Reference, By Mut Reference, By Value
let n = 5;
let f1 = || println!("{n}"); // &n
let mut m = 0;
let mut f2 = || m += 1; // &mut m
let s = String::from("x");
let f3 = || drop(s); // moves s out — FnOnce
let f4 = move || println!("{n}"); // copies n (i32 is Copy)
The compiler picks the least restrictive capture mode by default. move forces everything to move (or copy if Copy).
Higher-Order Closures
fn make<F: Fn(i32) -> i32>(f: F) -> impl Fn(i32) -> i32 {
move |x| f(x) + 1
}
Functions returning closures returning closures — typical in functional pipelines.
partial / Currying
Rust doesn't have built-in currying, but move closures make it easy:
let add = |a: i32| move |b: i32| a + b;
let add5 = add(5);
add5(3); // 8
Edge Cases & Pitfalls
- Capture lifetime: a closure borrowing from local vars can't escape the local's scope. Use
move(often with'staticrequirement). FnOnceandVec::map:mapconsumes the iterator but only requiresFnMut; if you consume captures inside, you might need a different signature.FnvsFnMutvsFnOncematching: passing aFnOnceclosure to a function expectingFnwon't compile. PassFnwhen possible.- Recursive closure: not directly possible; use a
fninstead. - Closures with
&mut self: a method that takes a closure that also borrowsselfmutably conflicts. Restructure (e.g., extract a value first). - Capturing by
RefCell: if you need to mutate through a closure called multiple times behind an&-reference, useRefCellfor interior mutability. movedoesn't always move:move || println!("{x}")forx: i32copies;moveonly forces by-value capture (which isCopy-duplicating forCopytypes).move ||in threads: required forstd::thread::spawnsince the closure must be'static + Send.
thread::spawn and 'static + Send
let data = vec![1, 2, 3];
std::thread::spawn(move || {
println!("{:?}", data); // OK — data moved in
});
Without move, you'd borrow data, which doesn't satisfy 'static. move + 'static + Send is the recipe for thread closures.
Summary
Closures capture by ref (Fn), mut ref (FnMut), or value (FnOnce). move forces by-value. Use impl Fn(...)/FnMut/FnOnce for generic APIs. Closures are essential for iterator combinators and async. Returning closures requires impl Fn (single type) or Box<dyn Fn> (heterogeneous).
Next: Error handling — Result, Option, and ?.