Closures in Rust
Deep dive into Fn, FnMut, FnOnce, environment capture, and move closures in Rust.
Closure Syntax
Closures are anonymous functions you can define inline and pass around as values. Unlike regular functions, they can capture variables from the surrounding scope, which makes them ideal for callbacks, iterators, and event handlers. The compiler infers parameter and return types from context, so you rarely need annotations.
fn main() {
// Full annotation — rarely needed
let add: fn(i32, i32) -> i32 = |a: i32, b: i32| -> i32 { a + b };
// Types inferred from usage
let multiply = |a, b| a * b;
// Single-expression body — no braces needed
let square = |x: i64| x * x;
// Multi-line body with braces
let clamp = |x: f64, lo: f64, hi: f64| {
if x < lo { lo }
else if x > hi { hi }
else { x }
};
println!("{}", add(3, 4)); // 7
println!("{}", multiply(3_i32, 4)); // 12
println!("{}", square(9)); // 81
println!("{}", clamp(15.0, 0.0, 10.0)); // 10.0
}
Capturing the Environment
The defining feature of closures over regular functions is environment capture — a closure can read and write variables from the scope it was created in. The compiler automatically chooses the least restrictive capture mode: it borrows immutably if it can, mutably if it must, and moves ownership only as a last resort. This keeps the original variables usable whenever possible.
fn main() {
let factor = 3;
let base = 100;
// Captures factor and base by immutable reference — both remain usable
let compute = |x| base + x * factor;
println!("{}", compute(5)); // 115
println!("{}", compute(10)); // 130
println!("{}", factor); // still accessible
}
The compiler’s capture priority:
| Priority | Mode | Trait |
|---|---|---|
| 1 (prefer) | Immutable borrow &T | Fn |
| 2 | Mutable borrow &mut T | FnMut |
| 3 (last resort) | Move (take ownership) | FnOnce |
The Three Closure Traits
FnOnce — Called At Most Once
FnOnce is the most permissive bound — it only requires that the closure can be called once. A closure that moves a captured value out of itself implements only FnOnce, because after that move the value is gone and calling the closure again would be undefined behavior. The compiler enforces the “at most once” rule at compile time.
fn consume_string<F: FnOnce() -> String>(f: F) {
let result = f();
println!("{}", result);
// f(); // ERROR: cannot call an FnOnce closure more than once
}
fn main() {
let greeting = String::from("Hello, Rust!");
// This closure moves greeting — it can only be called once
let f = || greeting;
consume_string(f);
// println!("{}", greeting); // ERROR: greeting was moved into the closure
}
FnMut — Called Multiple Times, Mutates Captured State
FnMut allows repeated calls while letting the closure mutate captured variables. Accumulation patterns — incrementing a counter, building a string, collecting results — use FnMut. The caller must declare the binding mut to signal that calling the closure changes state.
fn apply_n_times<F: FnMut()>(mut f: F, n: usize) {
for _ in 0..n {
f();
}
}
fn main() {
let mut count = 0;
// Mutably captures count — must be called through a mut binding
apply_n_times(|| {
count += 1;
println!("call #{}", count);
}, 3);
println!("final count: {}", count); // 3
}
Fn — Called Multiple Times, No Mutation
Fn is the most restrictive bound and the most flexible to use. A closure that only reads captured values — never mutates, never consumes — implements Fn. These closures can safely be called from multiple threads, stored in shared state, and reused freely. When writing a function that takes a callback, reach for Fn if you don’t need mutation.
fn apply_to_vec<F: Fn(i32) -> i32>(v: &[i32], f: F) -> Vec<i32> {
v.iter().map(|&x| f(x)).collect()
}
fn main() {
let offset = 10;
// Immutably captures offset — can be called any number of times
let result = apply_to_vec(&[1, 2, 3, 4, 5], |x| x + offset);
println!("{:?}", result); // [11, 12, 13, 14, 15]
println!("{}", offset); // offset still usable
}
move Closures
move forces the closure to take ownership of all captured variables, regardless of whether ownership is strictly needed. This is required any time the closure must outlive the scope where the variables were defined — the most common case is spawning threads, where the closure runs on a different thread that may outlast the function that created it. Without move, the borrow would dangle.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// Without move, data is borrowed — but the thread could outlive main(),
// making the borrow dangle. move transfers ownership to the closure.
let handle = thread::spawn(move || {
println!("{:?}", data); // data is owned by this closure/thread
});
// println!("{:?}", data); // ERROR: data was moved into the closure
handle.join().unwrap();
}
Factory Functions with move
move closures are also how you build factory functions that return closures carrying their own state. Each call to the factory creates a new closure with its own copy of the captured value, so the closures are independent.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n // n is moved into the closure; each call to make_adder gets a fresh n
}
fn make_multiplier(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x * n)
}
fn main() {
let add5 = make_adder(5);
let add10 = make_adder(10);
let triple = make_multiplier(3);
println!("{}", add5(3)); // 8
println!("{}", add10(3)); // 13
println!("{}", triple(7)); // 21
// Closures compose naturally
let add5_then_triple = |x| triple(add5(x));
println!("{}", add5_then_triple(4)); // (4+5)*3 = 27
}
Closures as Return Values
Returning a closure from a function requires naming its type. Use impl Fn(...) for static dispatch when there is one possible closure type, and Box<dyn Fn(...)> for dynamic dispatch when the closure returned depends on a runtime condition. The latter is slightly heavier due to heap allocation and vtable dispatch, but necessary when the return type genuinely varies.
// impl Fn — zero-cost, but must return the same closure shape every time
fn make_greeting(prefix: &str) -> impl Fn(&str) -> String + '_ {
move |name| format!("{}, {}!", prefix, name)
}
// Box<dyn Fn> — necessary when the returned closure differs per branch
fn make_transform(kind: &str) -> Box<dyn Fn(i32) -> i32> {
match kind {
"double" => Box::new(|x| x * 2),
"square" => Box::new(|x| x * x),
"negate" => Box::new(|x| -x),
_ => Box::new(|x| x),
}
}
fn main() {
let greet = make_greeting("Hello");
println!("{}", greet("Alice")); // Hello, Alice!
let transform = make_transform("square");
println!("{}", transform(7)); // 49
}
Storing Closures in Structs
Storing a closure in a struct is a common pattern for lazy evaluation, caching, and strategy objects. Using a generic parameter with a trait bound gives static dispatch; using Box<dyn Fn> gives runtime flexibility at the cost of a heap allocation per closure.
// Generic parameter — the closure type is baked in at compile time (zero overhead)
struct Cache<F: Fn(i32) -> i32> {
func: F,
cache: std::collections::HashMap<i32, i32>,
}
impl<F: Fn(i32) -> i32> Cache<F> {
fn new(func: F) -> Self {
Self { func, cache: std::collections::HashMap::new() }
}
fn call(&mut self, arg: i32) -> i32 {
// Compute once and memoize — subsequent calls return the cached result
*self.cache.entry(arg).or_insert_with(|| (self.func)(arg))
}
}
fn main() {
let mut cache = Cache::new(|x| {
println!("computing for {}", x);
x * x
});
println!("{}", cache.call(4)); // computing for 4 → 16
println!("{}", cache.call(4)); // cached → 16 (no "computing" print)
println!("{}", cache.call(5)); // computing for 5 → 25
}
Closure Type Inference Rules
Every closure has a unique anonymous type generated by the compiler — two closures with identical bodies are still different types. This means you cannot store two closures in a Vec unless you erase their types. Use fn pointers for non-capturing closures, and Box<dyn Fn> for closures that capture variables.
fn main() {
// These two closures have different types even though they look the same
let f = |x: i32| x + 1;
let g = |x: i32| x + 1;
// let v = vec![f, g]; // ERROR — f and g are different types
// Non-capturing closures coerce to function pointers
let ops: Vec<fn(i32) -> i32> = vec![
|x| x + 1,
|x| x * 2,
|x| x * x,
];
for op in &ops {
print!("{} ", op(3)); // 4 6 9
}
println!();
// Capturing closures require Box<dyn Fn>
let base = 100;
let ops2: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(move |x| x + base),
];
for op in &ops2 {
print!("{} ", op(5)); // 6 105
}
println!();
}
Practical: Event Dispatcher
A common real-world use of closures is an event system where multiple handlers can subscribe to an event. Each handler is a closure that captures its own state, enabling a flexible plugin-style architecture without inheritance or virtual dispatch overhead beyond what Box<dyn Fn> already implies.
struct EventBus {
handlers: Vec<Box<dyn Fn(&str)>>,
}
impl EventBus {
fn new() -> Self { Self { handlers: Vec::new() } }
// Accept any closure that takes a &str, owns its captures, and lives for 'static
fn on(&mut self, handler: impl Fn(&str) + 'static) {
self.handlers.push(Box::new(handler));
}
fn emit(&self, event: &str) {
for handler in &self.handlers {
handler(event);
}
}
}
fn main() {
let mut bus = EventBus::new();
bus.on(|e| println!("[Logger] event: {}", e));
let prefix = "APP";
bus.on(move |e| println!("[{}] received: {}", prefix, e));
bus.emit("user_login");
bus.emit("page_view");
}