Rust Closures
Closure Syntax
Anonymous functions that capture their environment
// Full syntax
let add = |a: i32, b: i32| -> i32 { a + b };
// Type inference — usually no annotations needed
let add = |a, b| a + b;
let result = add(2, 3); // compiler infers i32
// Multi-line closure
let process = |name: &str| {
let upper = name.to_uppercase();
println!("Processing: {upper}");
upper
};
Closures infer parameter and return types from usage. Once a closure is called with specific types, those types are fixed.
Capturing Variables
Closures capture variables from their environment
let name = String::from("Evan");
// Borrow immutably (Fn)
let greet = || println!("Hello, {name}");
greet();
println!("{name}"); // name still valid
// Borrow mutably (FnMut)
let mut count = 0;
let mut increment = || { count += 1; };
increment();
increment();
println!("{count}"); // 2
// Take ownership (FnOnce)
let name = String::from("Evan");
let consume = move || {
println!("consumed: {name}");
drop(name);
};
consume();
// name is no longer valid here
The compiler infers the minimal capture mode. move forces ownership transfer — required when the closure outlives the captured variables (threads, async).
Closure Traits
Fn, FnMut, FnOnce — the closure hierarchy
// Fn — borrows immutably, can be called multiple times
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
let doubled = apply_twice(|x| x * 2, 3); // 12
// FnMut — borrows mutably, can be called multiple times
fn apply_to_each(mut f: impl FnMut(i32), items: &[i32]) {
for &item in items {
f(item);
}
}
// FnOnce — may consume captured data, can be called only once
fn execute(f: impl FnOnce() -> String) -> String {
f()
}
Fn is a subset of FnMut, which is a subset of FnOnce. A function accepting FnOnce is the most flexible — it accepts any closure. A function accepting Fn is the most restrictive.
Closures with Iterators
map, filter, and friends take closures
let numbers = vec![1, 2, 3, 4, 5];
// map with closure
let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();
// filter with closure
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
// sort_by with closure
let mut names = vec!["charlie", "alice", "bob"];
names.sort_by(|a, b| a.len().cmp(&b.len()));
// for_each — closure instead of for loop
numbers.iter().for_each(|x| println!("{x}"));
Returning Closures
impl Fn or Box<dyn Fn>
// Return closure with impl (when type is known at compile time)
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
// Return boxed closure (when type varies at runtime)
fn make_formatter(prefix: &str) -> Box<dyn Fn(&str) -> String + '_> {
Box::new(move |msg| format!("[{prefix}] {msg}"))
}
impl Fn uses static dispatch. Box<dyn Fn> uses dynamic dispatch — needed when the concrete type differs between branches.
move Closures
Transfer ownership into the closure
use std::thread;
let data = vec![1, 2, 3];
// move is required — thread may outlive the current scope
let handle = thread::spawn(move || {
println!("{:?}", data);
});
// data is no longer accessible here
handle.join().unwrap();
move transfers ownership of all captured variables into the closure. Required for closures sent to threads or returned from functions.