Why this matters
Many APIs take a small piece of behavior as an argument: how to compare two items when sorting, which elements to keep when filtering, what to compute for each element, what to do on an error. Closures are anonymous functions written inline, and unlike ordinary functions they can use variables from the surrounding code. You have seen a few already (sort_by(|a, b| ...), map_err(|e| ...)). This lesson explains them properly, including how they capture variables, which connects back to ownership and borrowing.
Writing closures
fn main() {
let add_one = |x: i32| x + 1;
let add = |a, b| a + b;
let greet = |name: &str| {
let msg = format!("hello, {name}");
msg.to_uppercase()
};
let answer = || 42;
println!("{} {} {} {}", add_one(5), add(2, 3), greet("ada"), answer());
}
6 5 HELLO, ADA 42
|params| expressionor|params| { block }. Nofn, no name; you store it in a variable or pass it directly.- Parameter and return types are usually inferred from how the closure is used.
addgetsi32parameters from the calladd(2, 3). Once inferred, the types are fixed: callingadd(1.5, 2.0)afterwards would be an error. - You call a closure like a function.
Capturing the environment
The defining feature: a closure can use variables from where it is defined.
fn main() {
let threshold = 10;
let big = |x: &i32| *x > threshold;
let nums = [3, 12, 7, 25];
let count = nums.iter().filter(|x| big(x)).count();
println!("{count} numbers above {threshold}");
}
2 numbers above 10
big uses threshold, which is not a parameter. An ordinary fn cannot do that:
fn main() {
let threshold = 10;
fn big(x: i32) -> bool {
x > threshold
}
println!("{}", big(12));
}
error[E0434]: can't capture dynamic environment in a fn item
--> main.rs:4:13
|
4 | x > threshold
| ^^^^^^^^^
|
= help: use the `|| { ... }` closure form instead
How capturing works
The compiler turns each closure into an anonymous struct holding the captured variables, plus a method that runs the body. Conceptually:
let threshold = 10;
let big = |x: &i32| *x > threshold;
// becomes something like:
struct BigClosure<'a> { threshold: &'a i32 }
impl BigClosure<'_> {
fn call(&self, x: &i32) -> bool { *x > *self.threshold }
}
let big = BigClosure { threshold: &threshold };
A closure captures each variable in the least powerful way its body needs:
- by shared reference
&T, if it only reads the variable, - by mutable reference
&mut T, if it modifies it, - by value (move), if it consumes it (for example, returns it or drops it).
All the normal borrowing rules apply to those captured references:
fn main() {
let mut total = 0;
let mut add = |x: i32| total += x;
add(5);
add(10);
println!("total = {total}");
}
total = 15
add holds a &mut total, so the closure itself must be declared mut (calling it changes its captured state). While add is still going to be used, total cannot be read; after its last use, the borrow ends and the println! is fine. Use total between the two calls and you get the familiar E0502.
move closures
The move keyword forces every captured variable to be captured by value:
fn make_greeter(greeting: String) -> impl Fn(&str) -> String {
move |name| format!("{greeting}, {name}!")
}
fn main() {
let hi = make_greeter(String::from("Hi"));
let hey = make_greeter(String::from("Hey"));
println!("{}", hi("Ada"));
println!("{}", hey("Bob"));
}
Hi, Ada!
Hey, Bob!
Without move, the closure would borrow greeting, a local of make_greeter that is dropped when the function returns, so the returned closure would dangle. The compiler catches it:
fn make_greeter(greeting: String) -> impl Fn(&str) -> String {
|name| format!("{greeting}, {name}!")
}
fn main() {
println!("{}", make_greeter(String::from("Hi"))("Ada"));
}
error[E0373]: closure may outlive the current function, but it borrows `greeting`, which is owned by the current function
--> main.rs:2:5
|
2 | |name| format!("{greeting}, {name}!")
| ^^^^^^ -------- `greeting` is borrowed here
| |
| may outlive borrowed value `greeting`
|
note: closure is returned here
--> main.rs:2:5
|
2 | |name| format!("{greeting}, {name}!")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `greeting` (and any other referenced variables), use the `move` keyword
|
2 | move |name| format!("{greeting}, {name}!")
| ++++
E0373 with the fix spelled out. You need move whenever a closure must outlive the scope it was created in: returning closures, and passing them to threads (module 14). For Copy types, move copies; for others, it moves ownership into the closure, and the original variable can no longer be used.
-> impl Fn(&str) -> String says "returns some closure taking a &str and returning a String"; every closure has its own unnameable type, so impl Trait is how you return one. The next lesson covers Fn and its siblings.
Closures as arguments
Functions in the standard library take closures for customization. Some you will use constantly:
fn main() {
let mut words = vec!["banana", "Apple", "cherry", "date"];
words.sort_by_key(|w| w.to_lowercase());
println!("{:?}", words);
words.retain(|w| w.len() > 4);
println!("{:?}", words);
let lens: Vec<usize> = words.iter().map(|w| w.len()).collect();
println!("{:?}", lens);
let name: Option<&str> = None;
println!("{}", name.unwrap_or_else(|| "anonymous"));
let first_long = words.iter().find(|w| w.starts_with('c'));
println!("{:?}", first_long);
}
["Apple", "banana", "cherry", "date"]
["Apple", "banana", "cherry"]
[5, 6, 6]
anonymous
Some("cherry")
Note unwrap_or_else(|| ...): the closure runs only if needed, unlike unwrap_or(value), which evaluates its argument eagerly. Passing behavior as a closure lets the callee decide when (and whether) to run it.
Common mistakes
- Calling a capturing closure that is not
mut: E0596 "cannot borrowaddas mutable". Declare itlet mut add. - Borrow conflicts with captured variables: a closure holding
&mut xblocks other uses ofxuntil its last call. Finish with the closure first, or restructure. - Returning a closure without
move: E0373. Addmove. - Expecting closures to be generic:
let id = |x| x; id(1); id("a");fails: the first call fixes the type.
Recap
|args| bodycreates a closure; types are inferred and then fixed.- Closures capture variables from their environment: by
&,&mutor value, whichever the body needs. - A closure is an anonymous struct of its captures; borrowing rules apply to them.
movecaptures everything by value; required when the closure outlives its scope (returning, threads).- Closures customize library functions:
sort_by_key,retain,map,filter,find,unwrap_or_else.
Going further: read the Rust Book chapter 13.1. Then write a function that takes a Vec<i32> and a closure and returns a new Vec with the closure applied to each element (you will need the Fn bound from the next lesson).