Closures - Introduction
A closure (also called an anonymous function or lambda expression) is an inline callable value written with vertical pipes |params| body that can both accept parameters and capture variables from the enclosing scope in which it is defined.
Closure Syntax and Type Inference
Whereas a fn declaration uses parentheses (params) and requires explicit parameter and return type annotations, a closure lists its parameters between vertical pipes |...| and lets the compiler infer parameter and return types from how the closure is used:
fn add_fn (a: i32, b: i32) -> i32 { a + b } <-- Named function (types required)
let add_typed = |a: i32, b: i32| -> i32 { a + b }; <-- Closure with explicit types and braces
let add_short = |a, b| a + b; <-- Idiomatic closure (types inferred, single expr)
let zero_args = || 42; <-- Zero-parameter closure (`||` = empty pipes)
fn main() {
// 1. Single-expression closure with inferred types:
let double = |x| x * 2;
println!("double(21) = {}", double(21));
// 2. Multi-line block closure with explicit annotations:
let clamp_positive = |n: i32, max: i32| -> i32 {
if n < 0 {
0
} else if n > max {
max
} else {
n
}
};
println!("clamp_positive(-5, 10) = {}", clamp_positive(-5, 10));
// 3. Zero-argument closure (`||`):
let greet = || println!("Hello from a zero-argument closure!");
greet();
}
double(21) = 42
clamp_positive(-5, 10) = 0
Hello from a zero-argument closure!
First-Use Type Locking on Inferred Closures
Although let id = |x| x; looks generic, a closure is not polymorphic: the very first time you call id("hello"), the compiler locks its parameter and return types to &str. Calling id(42) afterward fails with E0308:
// ❌ does not compile
fn main() {
let identity = |x| x;
let _text = identity("hello"); // Locks `identity` to `fn(&str) -> &str`!
let _num = identity(42); // Error: expected `&str`, found integer!
}
error[E0308]: mismatched types
--> src/main.rs:5:25
|
5 | let _num = identity(42);
| -------- ^^ expected `&str`, found integer
| |
| arguments to this function are incorrect
|
note: closure parameter defined here
--> src/main.rs:2:21
|
2 | let identity = |x| x;
| ^
error: aborting due to 1 previous error
Capturing the Environment
The defining superpower of a closure over a nested fn item is that a closure can refer directly to local variables from its enclosing scope.
By default, rustc inspects how the closure body uses each captured variable and automatically chooses the least restrictive capture mode needed:
| Capture Mode | Trait Implemented | When Chosen Automatically by rustc |
Example Inside Closure Body |
|---|---|---|---|
1. Shared Borrow (&T) |
Fn (+ FnMut, FnOnce) |
Closure only reads the captured variable | println!("{threshold}"); |
2. Mutable Borrow (&mut T) |
FnMut (+ FnOnce) |
Closure mutates the captured variable in place | counter += 1; or list.push(x); |
3. By Value / Move (T) |
FnOnce |
Closure moves or drops the captured variable (or uses move ||) |
drop(owned_string); |
fn main() {
// 1. Capturing by shared reference (`&i32`):
let factor = 10;
let scale = |x: i32| x * factor;
println!("scale(4) = {} (factor is still accessible: {factor})", scale(4));
// 2. Capturing by mutable reference (`&mut u32`):
// Note that `record_visit` itself must be declared `let mut` to call a `FnMut` closure!
let mut visits = 0;
let mut record_visit = || {
visits += 1;
println!("Visit #{visits}");
};
record_visit();
record_visit();
println!("Total visits after closure finished: {visits}");
}
scale(4) = 40 (factor is still accessible: 10)
Visit #1
Visit #2
Total visits after closure finished: 2
Forcing Ownership Transfer with the move Keyword
Adding the move keyword before the parameter pipes (move |x| ...) forces the closure to take ownership of all variables it references from the outer scope (copying Copy types and moving non-Copy types into the closure's anonymous struct).
move closures are essential when returning a closure from a function or spawning a thread (std::thread::spawn(move || ...)), where the closure outlives the stack frame in which it was created:
fn make_prefixer(prefix: String) -> impl Fn(&str) -> String {
// `move` transfers ownership of `prefix` into the returned closure:
move |word: &str| format!("{prefix}: {word}")
}
fn main() {
let tag_info = make_prefixer(String::from("[INFO]"));
println!("{}", tag_info("Server started"));
println!("{}", tag_info("Listening on port 8080"));
}
[INFO]: Server started
[INFO]: Listening on port 8080
Closures with Standard Library Iterators and Option / Result
Closures are the primary building blocks for expressive iterator pipelines and Option/Result combinators:
fn main() {
let numbers = [1, 2, 3, 4, 5, 6];
let min_threshold = 3;
// The closure `|&n| n >= min_threshold` captures `min_threshold` from `main`!
let filtered_doubled: Vec<i32> = numbers
.iter()
.copied()
.filter(|&n| n >= min_threshold)
.map(|n| n * 2)
.collect();
let fallback = Some(10).map(|v| v + 5).unwrap_or_else(|| 0);
println!("filtered_doubled: {filtered_doubled:?}, fallback: {fallback}");
}
filtered_doubled: [6, 8, 10, 12], fallback: 15