Closures and captures
In Rust a closure is a function without a name that captures values from the scope where it is written. The syntax is a pipe list of parameters, then an expression: |a, b| a + b. The compiler infers the parameter and return types from the first use, and the closure can be stored, passed and called like a value.
Closures are what makes iterator chains, sort_by and the callbacks in the next part of the course readable. This lesson covers the syntax, the two capture modes, the move keyword, a closure passed as an argument and the error for a closure that outlives what it borrows.
A closure
The closure below takes two integers and returns their sum. The types are inferred from the call, and the closure is bound to a name so it can be called twice.
The program calls it with two pairs and prints the two sums. Nothing is allocated, and the call is direct.
fn main() {
let add = |a: i32, b: i32| a + b;
println!("{}", add(2, 3));
}
rustc --edition 2024 closure.rs
./closure
5
The compiler gives the closure its own anonymous type, which is why two closures with the same body are still different types and why a closure cannot be named in a struct field without a trait object or a function pointer. The inferred types are fixed at the first call, so a second call with another type is E0308. A generic closure needs a function or a generic parameter, which the lesson on generics shows.
Capturing from the scope
A closure may use a variable from the enclosing scope without declaring it as a parameter. The capture is by reference by default, so the closure borrows the value and the owner keeps it.
The program captures a factor and multiplies by it. The factor is printed after the call, which proves that the closure only borrowed it.
fn main() {
let factor = 10;
let scale = |value: i32| value * factor;
println!("{}", scale(3));
println!("{factor}");
}
rustc --edition 2024 capture.rs
./capture
30
10
A closure that writes to a captured variable is a FnMut, and calling it needs a mutable binding for the closure itself. That is the same rule as a method on &mut self: the call borrows the closure mutably, and the borrow ends when the statement does. While the closure is alive the captured value is borrowed, so the program may not change it. The lesson on mutable borrows explains the rule, and a closure that writes needs a mutable closure.
The move keyword
move |...| takes ownership of everything it captures. That is how a closure can be returned from a function or sent to a thread, because it no longer borrows the caller scope.
The program moves a string into a closure and calls it. The closure owns the string, and the string is released when the closure goes out of scope.
fn main() {
let name = String::from("rust");
let show = move || println!("{name}");
show();
}
rustc --edition 2024 move_closure.rs
./move_closure
rust
The printed value is the one the closure owns. The lesson on ownership covers the move itself, and the move closure is the form the threads lesson uses with thread::spawn.
A closure as an argument
A function that takes a closure names the trait it needs: Fn, FnMut or FnOnce. The sorting method of a vector takes a comparison closure, and the compiler checks that the closure matches the signature.
The program sorts three words by length. The closure compares the two lengths, and the sort reorders the vector in place.
fn main() {
let mut words = vec!["pear", "fig", "banana"];
words.sort_by(|a, b| a.len().cmp(&b.len()));
println!("{words:?}");
}
rustc --edition 2024 sort_by.rs
./sort_by
["fig", "pear", "banana"]
The comparison returns an Ordering, which is what a.len().cmp(&b.len()) produces. The closure may do any work that ends in that value, so a sort by a computed key is one line. The three words come out shortest first. The same call with |a, b| a.cmp(b) would sort alphabetically, which is the default the method already provides.
A closure that outlives its borrow
A closure that borrows a value keeps the borrow alive, so moving the value while the closure exists is E0505. The message names the closure as the place the borrow occurs, which is the part that surprises people.
The program creates a closure, drops the captured string and then calls the closure. The compiler rejects the drop, not the call, because that is where the promise breaks.
fn main() {
let name = String::from("rust");
let show = || println!("{name}");
drop(name);
show();
}
rustc --edition 2024 borrow_err.rs
error[E0505]: cannot move out of `name` because it is borrowed
--> borrow_err.rs:4:10
|
3 | let show = || println!("{name}");
| -- ---- borrow occurs due to use in closure
| |
| borrow of `name` occurs here
4 | drop(name);
| ^^^^ move out of `name` occurs here
5 | show();
| ---- borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0505`.
The fix is to call the closure before the drop, or to use move so that the closure owns the value and outlives the binding. The lesson on borrowing explains why the borrow has to end first.
Exercises
- Write a closure that adds ten to its argument and call it three times.
- Sort a vector of numbers by their distance from ten with a closure. Which method do you need?
- Return a closure from a function. Which keyword does the closure need, and what does the return type look like?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a closure?
Answer
An anonymous function that can capture its surroundings
The closure body sees the names of the enclosing scope, which a function cannot.
-
How does a closure capture a variable by default?
Answer
By reference
The default is the least restrictive capture, a shared borrow, and move changes it.
-
What does the move keyword do on a closure?
Answer
It takes ownership of the captured values
The closure owns its captures, which lets it outlive the scope where it was written.
-
Which trait does a closure passed to sort_by implement?
Answer
FnMut, because the sort calls it several times
The comparison is called many times and does not consume its captures.
-
What does this program print?
fn main() { let add = |a: i32, b: i32| a + b; println!("{}", add(2, 3)); }Answer
5
The closure is bound to add and returns the sum of the two arguments.
-
What does this program print?
fn main() { let factor = 10; let scale = |value: i32| value * factor; println!("{}", scale(3)); println!("{factor}"); }Answer
30 / 10
The closure borrows the factor, and the owner still prints it afterwards.
-
What does this program print?
fn main() { let name = String::from("rust"); let show = move || println!("{name}"); show(); }Answer
rust
The closure owns the string and prints it when called.
-
What does this program print?
fn main() { let mut words = vec!["pear", "fig", "banana"]; words.sort_by(|a, b| a.len().cmp(&b.len())); println!("{words:?}"); }Answer
["fig", "pear", "banana"]
The comparison sorts by length, so the three-letter and four-letter words come first.
-
What is the first line of the compiler output for this program?
fn main() { let name = String::from("rust"); let show = || println!("{name}"); drop(name); show(); }Answer
error[E0505]: cannot move out of
namebecause it is borrowedThe closure holds a borrow of name, so the drop while it is alive is rejected.
-
What does this program print?
fn main() { let mut count = 0; let mut bump = || count += 1; bump(); bump(); println!("{count}"); }Answer
2
The closure captures count by mutable reference, and two calls leave it at two.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev