Rust Closures
A closure is a function you can write inline and store in a variable. Unlike regular functions, closures can capture variables from the surrounding code. They are especially useful with iterators, event handlers, and any situation where you need a short, throwaway piece of logic.
Writing a Closure
let add = |x, y| x + y;
println!("{}", add(3, 4)); ← 7
The vertical bars |x, y| contain the parameters. The expression after them is the body. No braces needed for a single expression.
Closure vs Function Comparison
fn add_fn(x: i32, y: i32) -> i32 { x + y } ← Regular function
let add_cl = |x: i32, y: i32| -> i32 { x + y }; ← Closure, explicit types
let add_cl = |x, y| x + y; ← Closure, types inferred
The Sticky Note Diagram
Regular function = A recipe in a cookbook
Anyone can use it, always available
Closure = A sticky note with instructions
Stays near the code that uses it
Can refer to things nearby (captured variables)
Capturing the Environment
A closure can use variables from the scope where it is defined. Regular functions cannot do this.
fn main() {
let threshold = 10;
let is_big = |n| n > threshold; ← Captures threshold from outer scope
println!("{}", is_big(15)); ← true
println!("{}", is_big(5)); ← false
}
threshold is not a parameter of the closure — the closure captures it from the environment automatically.
How Closures Capture
Rust closures capture variables in three ways depending on how the closure uses them:
Capture Mode Keyword When Used ------------ ------- --------- By reference (auto) Read-only access to the captured value By mutable ref (auto) Needs to modify the captured value By move move Takes ownership of the captured value
Capture by Reference (Default)
let greeting = String::from("Hello");
let say_hi = || println!("{}", greeting); ← Borrows greeting
say_hi();
println!("{}", greeting); ← greeting still works here
Capture by Move
let greeting = String::from("Hello");
let say_hi = move || println!("{}", greeting); ← Takes ownership
say_hi();
// greeting is no longer usable here
Move closures are important when passing closures to threads, since the thread might outlive the current scope.
Closures as Function Parameters
Pass a closure to a function using one of the three function traits:
Trait Meaning ----- ------- Fn Can be called many times, borrows the environment FnMut Can be called many times, mutably borrows the environment FnOnce Can be called only once, takes ownership of the environment
fn apply<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
f(value)
}
fn main() {
let double = |x| x * 2;
println!("{}", apply(double, 5)); ← 10
}
Closures with Iterators
Closures shine when used with iterator methods. These methods accept closures to express what to do with each element.
map — Transform Each Element
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled); ← [2, 4, 6, 8, 10]
filter — Keep Only Matching Elements
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
println!("{:?}", evens); ← [2, 4, 6]
The Assembly Line Diagram
numbers: [1, 2, 3, 4, 5]
↓ map(|n| n * 2)
doubled: [2, 4, 6, 8, 10]
↓ filter(|&&n| n > 5)
result: [6, 8, 10]
Returning Closures
Return a closure from a function using impl Fn or boxing:
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let triple = make_multiplier(3);
println!("{}", triple(5)); ← 15
}
Quick Reference
|x| x + 1 ← Simple closure
|x: i32| -> i32 { x + 1 } ← Closure with explicit types
move |x| x + captured_val ← Moves captured variables
fn f<F: Fn(i32)>(f: F) ← Accept a closure as parameter
