Introduction
Closures are anonymous functions that can capture values from their surrounding scope. You’ve used them in iterator chains like .map() and .filter() — this post explains how they work under the hood, what the three closure traits mean, and how to write functions that accept or return closures.
Closure Syntax
Closures use |params| body — vertical bars instead of parentheses:
fn main() {
let add = |x, y| x + y;
println!("{}", add(2, 3)); // 5
let square = |x: i32| -> i32 {
x * x
};
println!("{}", square(4)); // 16
}
Types are inferred from usage. Explicit annotations are optional.
Capturing the Environment
Unlike regular functions, closures can read variables from the enclosing scope:
fn main() {
let threshold = 10;
let is_big = |n| n > threshold; // borrows threshold
println!("{}", is_big(15)); // true
println!("{}", is_big(5)); // false
}
The closure borrows threshold — no copy needed.
The Three Closure Traits
Every closure implements one or more of these traits, from most to least restrictive:
| Trait | Callable | What it does with captures |
|---|---|---|
FnOnce |
Once only | Moves (consumes) captured values |
FnMut |
Multiple times | Mutably borrows captures |
Fn |
Multiple times | Immutably borrows captures |
Every closure is at least FnOnce. If it doesn’t consume captures, it’s also FnMut. If it doesn’t mutate them, it’s also Fn.
FnOnce — Consuming Closures
fn run_once(f: impl FnOnce()) {
f();
// f(); // error: value used after move
}
fn main() {
let name = String::from("Alice");
let greet = move || println!("Hello, {}!", name); // moves name in
run_once(greet);
}
move forces ownership transfer into the closure. After run_once, both the closure and name are gone.
FnMut — Mutating Closures
fn apply_twice(mut f: impl FnMut()) {
f();
f();
}
fn main() {
let mut count = 0;
let bump = || {
count += 1;
println!("count = {}", count);
};
apply_twice(bump);
// count = 1
// count = 2
}
Fn — Shared Closures
Most closures you write are Fn — they just read captured values:
fn apply_to_5(f: impl Fn(i32) -> i32) -> i32 {
f(5)
}
fn main() {
let offset = 10;
let result = apply_to_5(|x| x + offset);
println!("{}", result); // 15
// offset is still usable here
}
Closures in Iterator Chains
The most common place you’ll encounter closures:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
println!("{:?}", result); // [4, 8, 12]
}
Returning Closures
Use impl Fn when there’s a single concrete closure type:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
println!("{}", add5(10)); // 15
}
Need to return different closure types at runtime? Use a trait object:
fn make_op(double: bool) -> Box<dyn Fn(i32) -> i32> {
if double {
Box::new(|x| x * 2)
} else {
Box::new(|x| x + 1)
}
}
Storing Closures in Structs
struct Callback {
action: Box<dyn Fn()>,
}
impl Callback {
fn new(f: impl Fn() + 'static) -> Self {
Callback { action: Box::new(f) }
}
fn run(&self) {
(self.action)();
}
}
fn main() {
let cb = Callback::new(|| println!("Triggered!"));
cb.run(); // Triggered!
cb.run(); // Triggered!
}
Exercises
- Write
apply_n(f: impl FnMut(), n: usize)that callsfexactlyntimes. - Write
make_multiplier(factor: i32) -> impl Fn(i32) -> i32— a factory that returns a closure multiplying its input byfactor. - Using only iterator methods (no explicit loops), take a
Vec<String>, keep strings longer than 4 chars, and collect their uppercase versions into a newVec<String>.
Summary
- Closures capture environment; regular functions don’t
movetransfers ownership of captured variables into the closure- Prefer
impl Fnin function signatures; useFnMutorFnOnceonly when needed - Return with
impl Fn(…)for single types;Box<dyn Fn(…)>for dynamic dispatch