Lesson 36 of 94
Closures
A closure is an anonymous function that can capture variables from the scope where it is defined. You have already used a few: |c| c.is_ascii_digit() in any, and || String::from(...) in unwrap_or_else.
The key idea is capturing. A Rust closure can capture by shared reference, by mutable reference, or by taking ownership, and the compiler tracks which one, applying the normal borrowing rules.
| Form | Rust |
|---|---|
| One parameter | |x| x + 1 |
| Two parameters | |a, b| a + b |
| No parameters | || "hi" |
| Block body | |x| { ...; y } |
| Closure type in a signature | impl Fn(i32) -> i32 |
| Captured locals | may be read, changed, or moved |
| Named function as a closure | String::len or str::len |
1. Closure syntax
Parameters go between vertical bars. The body is an expression, or a block for several statements.
fn main() {
let add_one = |x| x + 1;
let add = |a, b| a + b;
let greet = || String::from("hello");
let describe = |n: i32| {
let kind = if n % 2 == 0 { "even" } else { "odd" };
format!("{n} is {kind}")
};
println!("{} {} {} {}", add_one(41), add(2, 3), greet(), describe(7));
}42 5 hello 7 is odd2. Type annotations are optional
Closures infer their parameter and return types from use. You can annotate them, like a function signature, when it helps readability.
fn main() {
let area = |width: f64, height: f64| -> f64 { width * height };
let halve = |x| x / 2.0; // inferred as f64 from its use below
println!("{} {}", area(3.0, 4.5), halve(9.0));
}13.5 4.53. A closure's types are fixed after the first use
A closure is not generic. Once the compiler has inferred its types, calling it with another type is an error.
fn main() {
let echo = |x| x;
let s = echo(String::from("text"));
let n = echo(5);
}error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let n = echo(5);
| ---- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:3:18
|
3 | let s = echo(String::from("text"));
| ---- ^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:17
|
2 | let echo = |x| x;
| ^
help: try using a conversion method
|
4 | let n = echo(5.to_string());
| ++++++++++++4. Capturing by reference
A closure that only reads a variable borrows it. The variable stays usable, and other shared borrows can coexist.
fn main() {
let tax_rate = 0.2;
let currency = String::from("EUR");
let with_tax = |net: f64| format!("{:.2} {currency}", net * (1.0 + tax_rate));
println!("{}", with_tax(10.0));
println!("{}", with_tax(99.99));
println!("still usable: {currency} at {tax_rate}");
}12.00 EUR
119.99 EUR
still usable: EUR at 0.25. Capturing by mutable reference
A closure that changes a captured variable borrows it mutably. The closure itself must be declared mut, because calling it changes its state.
fn main() {
let mut clicks = 0;
let mut on_click = || {
clicks += 1;
println!("clicked {clicks} time(s)");
};
on_click();
on_click();
on_click();
println!("final count: {clicks}");
}clicked 1 time(s)
clicked 2 time(s)
clicked 3 time(s)
final count: 36. The borrow rules apply while the closure exists
While on_click holds a mutable borrow of clicks, nothing else can read it.
fn main() {
let mut clicks = 0;
let mut on_click = || clicks += 1;
on_click();
println!("{clicks}");
on_click();
}error[E0502]: cannot borrow `clicks` as immutable because it is also borrowed as mutable
--> src/main.rs:5:16
|
3 | let mut on_click = || clicks += 1;
| -- ------ first borrow occurs due to use of `clicks` in closure
| |
| mutable borrow occurs here
4 | on_click();
5 | println!("{clicks}");
| ^^^^^^ immutable borrow occurs here
6 | on_click();
| -------- mutable borrow later used here
|As usual, the borrow ends at the closure's last use, so moving the println! after the second call fixes it.
7. move closures take ownership
move makes the closure take ownership of everything it captures. The original variables can no longer be used (unless they are Copy).
fn main() {
let name = String::from("Ferris");
let greet = move || format!("hello, {name}");
println!("{}", greet());
println!("{name}");
}error[E0382]: borrow of moved value: `name`
--> src/main.rs:5:16
|
2 | let name = String::from("Ferris");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
3 | let greet = move || format!("hello, {name}");
| ------- ---- variable moved due to use in closure
| |
| value moved into closure here
4 | println!("{}", greet());
5 | println!("{name}");
| ^^^^ value borrowed here after move
|
help: consider cloning the value before moving it into the closure
|
3 ~ let value = name.clone();
4 ~ let greet = move || format!("hello, {value}");
|move is essential when the closure must outlive the current scope: when it is returned from a function, stored for later, or sent to another thread (Threads).
8. move copies Copy values
For Copy types, move copies the value into the closure. Changes inside the closure don't affect the original.
fn main() {
let mut counter = 10;
let mut bump = move || {
counter += 1;
counter
};
println!("{} {}", bump(), bump());
println!("original is still {counter}");
}11 12
original is still 109. Closures as arguments
Standard library methods take closures everywhere. Here are the ones you have already met, plus retain and sort_by_key.
fn main() {
let mut orders = vec![("ana", 120), ("ben", 0), ("cy", 45), ("dee", 300)];
orders.retain(|(_, total)| *total > 0);
orders.sort_by_key(|(_, total)| *total);
println!("{:?}", orders);
let big = orders.iter().filter(|(_, t)| *t >= 100).count();
let names: Vec<&str> = orders.iter().map(|(name, _)| *name).collect();
println!("{big} big orders, customers {:?}", names);
let first_name = names.first().map(|n| n.to_uppercase()).unwrap_or_else(|| String::from("NONE"));
println!("{first_name}");
}[("cy", 45), ("ana", 120), ("dee", 300)]
2 big orders, customers ["cy", "ana", "dee"]
CY10. Writing a function that takes a closure
Accept a closure with a trait bound such as impl Fn(i32) -> i32. Fn is one of three closure traits, explained in the next lesson.
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn apply_to_all(values: &[f64], f: impl Fn(f64) -> f64) -> Vec<f64> {
let mut out = Vec::new();
for &v in values {
out.push(f(v));
}
out
}
fn main() {
println!("{}", apply_twice(|x| x * 3, 2));
let offset = 100;
println!("{}", apply_twice(|x| x + offset, 1));
println!("{:?}", apply_to_all(&[1.0, 4.0, 9.0], f64::sqrt));
println!("{:?}", apply_to_all(&[20.0, 37.5], |c| c * 9.0 / 5.0 + 32.0));
}18
201
[1.0, 2.0, 3.0]
[68.0, 99.5]11. Passing named functions and methods
Anywhere a closure is expected, you can pass a function by name, or a method with the Type::method path.
fn is_valid_sku(s: &str) -> bool {
s.len() == 6 && s.starts_with("SK")
}
fn main() {
let raw = vec!["SK1234", "bad", "SK9999", "SKU"];
let checks: Vec<bool> = raw.iter().copied().map(is_valid_sku).collect();
let owned: Vec<String> = raw.iter().copied().map(String::from).collect();
let roots: Vec<f64> = [4.0, 9.0, 2.25].into_iter().map(f64::sqrt).collect();
println!("{:?}\n{:?}\n{:?}", checks, owned, roots);
let valid: Vec<&str> = raw.iter().copied().filter(|s| is_valid_sku(s)).collect();
println!("{:?}", valid);
}[true, false, true, false]
["SK1234", "bad", "SK9999", "SKU"]
[2.0, 3.0, 1.5]
["SK1234", "SK9999"]A named function must match the expected signature exactly. filter hands its closure a reference to each item (&&str here), which doesn't match fn(&str), so a small closure |s| is_valid_sku(s) adapts it. Auto-deref inside the closure takes care of the extra &.
12. Returning a closure
A function can return impl Fn(...). The closure must move the values it captures, because the function's locals disappear when it returns.
fn make_multiplier(factor: f64) -> impl Fn(f64) -> f64 {
move |x| x * factor
}
fn make_greeter(greeting: &str) -> impl Fn(&str) -> String {
let greeting = greeting.to_string();
move |name| format!("{greeting}, {name}!")
}
fn main() {
let double = make_multiplier(2.0);
let vat = make_multiplier(1.21);
println!("{} {:.2}", double(8.5), vat(100.0));
let hello = make_greeter("Hello");
let hola = make_greeter("Hola");
println!("{} / {}", hello("Ada"), hola("Grace"));
}17 121.00
Hello, Ada! / Hola, Grace!13. Forgetting move when returning a closure
Without move, the closure would borrow factor, a parameter that is dropped when the function returns.
fn make_multiplier(factor: f64) -> impl Fn(f64) -> f64 {
|x| x * factor
}
fn main() {
println!("{}", make_multiplier(2.0)(3.0));
}error[E0373]: closure may outlive the current function, but it borrows `factor`, which is owned by the current function
--> src/main.rs:2:5
|
2 | |x| x * factor
| ^^^ ------ `factor` is borrowed here
| |
| may outlive borrowed value `factor`
|
note: closure is returned here
--> src/main.rs:2:5
|
2 | |x| x * factor
| ^^^^^^^^^^^^^^
help: to force the closure to take ownership of `factor` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x * factor
| ++++14. Closures stored in struct fields
A struct can hold a closure through a generic type parameter.
struct Validator<F: Fn(&str) -> bool> {
name: &'static str,
check: F,
}
impl<F: Fn(&str) -> bool> Validator<F> {
fn run(&self, input: &str) -> String {
format!("{} {}: {}", self.name, input, if (self.check)(input) { "ok" } else { "rejected" })
}
}
fn main() {
let min_len = 5;
let v = Validator { name: "length", check: |s: &str| s.len() >= min_len };
println!("{}", v.run("rust"));
println!("{}", v.run("ferris"));
}length rust: rejected
length ferris: ok(self.check)(input) calls the closure stored in the field. The parentheses are needed, because self.check(input) would look for a method named check.
15. A list of different closures with Box<dyn Fn>
Every closure has its own unique, unnameable type, so two closures can't be in one Vec directly. Boxing them as trait objects solves it, exactly as in Trait Objects.
fn main() {
let banned = vec!["spam", "scam"];
let rules: Vec<(&str, Box<dyn Fn(&str) -> bool>)> = vec![
("not empty", Box::new(|s: &str| !s.trim().is_empty())),
("max 40 chars", Box::new(|s: &str| s.chars().count() <= 40)),
("no banned words", Box::new(move |s: &str| !banned.iter().any(|w| s.to_lowercase().contains(w)))),
];
for comment in ["Great article!", "", "Totally not a SCAM, click here"] {
let failed: Vec<&str> = rules.iter().filter(|(_, rule)| !rule(comment)).map(|(name, _)| *name).collect();
println!("{comment:?}: {}", if failed.is_empty() { String::from("accepted") } else { failed.join(", ") });
}
}"Great article!": accepted
"": not empty
"Totally not a SCAM, click here": no banned words16. Function pointers: fn(T) -> U
A plain function has a function pointer type, written fn(i32) -> i32 (lowercase fn). Closures that capture nothing also convert to function pointers. They are handy for tables of operations.
fn add(a: i64, b: i64) -> i64 {
a + b
}
fn main() {
let ops: [(&str, fn(i64, i64) -> i64); 4] = [
("+", add),
("-", |a, b| a - b),
("*", |a, b| a * b),
("max", i64::max),
];
for (name, op) in ops {
println!("12 {name} 5 = {}", op(12, 5));
}
}12 + 5 = 17
12 - 5 = 7
12 * 5 = 60
12 max 5 = 12A function pointer is always a plain pointer, so it has no captured state. A closure that captures variables can't become a fn pointer; use a generic impl Fn or Box<dyn Fn> instead.
17. Closures that change their captured state
A closure passed to a method can accumulate results in variables outside it.
fn main() {
let words = ["apple", "bob", "kayak", "rust", "level"];
let mut palindromes = Vec::new();
let mut checked = 0;
words.iter().for_each(|w| {
checked += 1;
if w.chars().eq(w.chars().rev()) {
palindromes.push(*w);
}
});
println!("checked {checked}, palindromes {:?}", palindromes);
}checked 5, palindromes ["bob", "kayak", "level"]18. Immediately invoked closures
Calling a closure right where you define it groups the steps of a complicated initialization, and lets you use ? inside it.
fn main() {
let raw_port = "8080";
let port: u16 = (|| {
let n: u32 = raw_port.trim().parse().ok()?;
u16::try_from(n).ok().filter(|p| *p >= 1024)
})()
.unwrap_or(3000);
println!("listening on {port}");
}listening on 808019. Closures capture individual fields
A closure that uses self.a captures only the field a, not the whole struct. This means a closure can read one field while you change another.
struct Report {
title: String,
lines: Vec<String>,
}
fn main() {
let mut report = Report { title: String::from("Weekly"), lines: Vec::new() };
let prefix = |text: &str| format!("[{}] {text}", report.title); // borrows only `title`
report.lines.push(prefix("sales up 4%")); // `lines` is still free to change
report.lines.push(prefix("churn down"));
println!("{:?}", report.lines);
}["[Weekly] sales up 4%", "[Weekly] churn down"]20. Sorting with a comparison closure
sort_by takes a closure returning an Ordering. Combine keys with then and reverse with .reverse() or by swapping a and b.
fn main() {
let mut products: Vec<(&str, f64, u32)> = vec![
("keyboard", 4.5, 129),
("mouse", 4.8, 39),
("monitor", 4.5, 299),
("webcam", 3.9, 79),
];
// highest rating first, then cheapest first
products.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.2.cmp(&b.2)));
for (name, rating, price) in &products {
println!("{name:<9} {rating} ${price}");
}
}mouse 4.8 $39
keyboard 4.5 $129
monitor 4.5 $299
webcam 3.9 $7921. Closures and Option/Result combinators
Most combinators take closures, and they chain into readable pipelines.
fn main() {
let raw_discount: Option<&str> = Some(" 15% ");
let discount = raw_discount
.map(|s| s.trim().trim_end_matches('%'))
.and_then(|s| s.parse::<u32>().ok())
.filter(|&p| p <= 50)
.unwrap_or(0);
println!("discount {discount}%");
let total: Result<u32, String> = "42".parse::<u32>().map(|n| n * 2).map_err(|e| format!("bad input: {e}"));
println!("{:?}", total);
}discount 15%
Ok(84)22. Realistic example: event callbacks
A tiny event emitter stores callbacks per event name. Each callback owns what it captured.
struct Emitter {
handlers: Vec<(String, Box<dyn Fn(&str) -> String>)>,
}
impl Emitter {
fn on(&mut self, event: &str, handler: impl Fn(&str) -> String + 'static) {
self.handlers.push((event.to_string(), Box::new(handler)));
}
fn emit(&self, event: &str, payload: &str) {
for (name, handler) in &self.handlers {
if name == event {
println!("{}", handler(payload));
}
}
}
}
fn main() {
let mut emitter = Emitter { handlers: Vec::new() };
let admin_email = String::from("admin@example.com");
emitter.on("signup", |user| format!("send welcome mail to {user}"));
emitter.on("signup", move |user| format!("notify {admin_email} about {user}"));
emitter.on("delete", |user| format!("schedule data export for {user}"));
emitter.emit("signup", "ana");
emitter.emit("delete", "ben");
emitter.emit("login", "cy");
}send welcome mail to ana
notify admin@example.com about ana
schedule data export for benimpl Fn(&str) -> String + 'static requires that the closure borrows nothing short-lived, since it is stored for later. The move closure satisfies that by owning admin_email.
23. Realistic example: a retry helper
The operation is a closure, so the helper works for any fallible action. It is FnMut because the operation may change state between attempts, a detail explained in the next lesson.
fn retry<T, E: std::fmt::Display>(max_attempts: u32, mut operation: impl FnMut(u32) -> Result<T, E>) -> Result<T, E> {
let mut attempt = 1;
loop {
match operation(attempt) {
Ok(value) => return Ok(value),
Err(e) if attempt < max_attempts => {
println!(" attempt {attempt} failed: {e}");
attempt += 1;
}
Err(e) => return Err(e),
}
}
}
fn main() {
let mut calls = 0;
let result = retry(5, |attempt| {
calls += 1;
if attempt < 3 { Err(format!("timeout #{attempt}")) } else { Ok("200 OK") }
});
println!("{:?} after {calls} calls", result);
let failed: Result<u32, &str> = retry(2, |_| Err("connection refused"));
println!("{:?}", failed);
} attempt 1 failed: timeout #1
attempt 2 failed: timeout #2
Ok("200 OK") after 3 calls
attempt 1 failed: connection refused
Err("connection refused")24. Realistic example: a configurable price pipeline
Each step is a closure, built from configuration at runtime. The final price is the result of running every step in order.
fn build_pipeline(member: bool, coupon: Option<u64>, tax_percent: u64) -> Vec<Box<dyn Fn(u64) -> u64>> {
let mut steps: Vec<Box<dyn Fn(u64) -> u64>> = Vec::new();
if member {
steps.push(Box::new(|cents| cents * 90 / 100));
}
if let Some(off) = coupon {
steps.push(Box::new(move |cents| cents.saturating_sub(off)));
}
steps.push(Box::new(move |cents| cents + cents * tax_percent / 100));
steps
}
fn main() {
let base = 10_000;
for (member, coupon) in [(false, None), (true, None), (true, Some(1_500))] {
let pipeline = build_pipeline(member, coupon, 20);
let price = pipeline.iter().fold(base, |acc, step| step(acc));
println!("member={member:<5} coupon={:?}: {} steps -> {}", coupon, pipeline.len(), price);
}
}member=false coupon=None: 1 steps -> 12000
member=true coupon=None: 2 steps -> 10800
member=true coupon=Some(1500): 3 steps -> 900025. Realistic example: wrapping a function with logging
A function that takes a closure and returns a new closure can add behavior around it, like a decorator.
fn logged<F>(name: &'static str, f: F) -> impl Fn(i64) -> i64
where
F: Fn(i64) -> i64,
{
move |x| {
let result = f(x);
println!("{name}({x}) = {result}");
result
}
}
fn main() {
let square = logged("square", |x| x * x);
let abs = logged("abs", i64::abs);
let total = square(4) + square(-3) + abs(-10);
println!("total {total}");
}square(4) = 16
square(-3) = 9
abs(-10) = 10
total 3526. Realistic example: grouping with a key function
A generic helper takes a closure that computes each item's group. The same helper groups orders by customer, words by length, or anything else.
fn group_by<T, K: PartialEq>(items: &[T], key: impl Fn(&T) -> K) -> Vec<(K, Vec<&T>)> {
let mut groups: Vec<(K, Vec<&T>)> = Vec::new();
for item in items {
let k = key(item);
match groups.iter_mut().find(|(gk, _)| *gk == k) {
Some((_, members)) => members.push(item),
None => groups.push((k, vec![item])),
}
}
groups
}
fn main() {
let orders = [("ana", 30), ("ben", 12), ("ana", 7), ("cy", 50), ("ben", 3)];
for (customer, list) in group_by(&orders, |o| o.0) {
let total: i32 = list.iter().map(|o| o.1).sum();
println!("{customer}: {} orders, total {total}", list.len());
}
let words = ["go", "rust", "java", "c", "zig"];
for (len, ws) in group_by(&words, |w| w.len()) {
println!("length {len}: {:?}", ws);
}
}ana: 2 orders, total 37
ben: 2 orders, total 15
cy: 1 orders, total 50
length 2: ["go"]
length 4: ["rust", "java"]
length 1: ["c"]
length 3: ["zig"]HashMap shows the faster way to group large inputs; the closure-based design is the same.
27. Realistic example: lazy defaults and expensive fallbacks
unwrap_or_else and or_else take closures so that expensive work only runs when needed. Here the "database" lookup runs only for cache misses.
fn main() {
let cache = [("user:1", "Ada"), ("user:2", "Grace")];
let mut db_queries = 0;
let mut lookup = |key: &str| -> String {
cache
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| v.to_string())
.unwrap_or_else(|| {
db_queries += 1;
format!("{key} (loaded from db)")
})
};
for key in ["user:1", "user:3", "user:2", "user:4"] {
println!("{}", lookup(key));
}
println!("database queries: {db_queries}");
}Ada
user:3 (loaded from db)
Grace
user:4 (loaded from db)
database queries: 2Summary
- Closures are written
|params| body. Types are inferred once and then fixed. - A closure captures what it uses: by shared reference if it only reads, by mutable reference if it changes a variable (the closure must then be
mut), or by value withmove. - Borrow rules apply to captures while the closure exists.
- Accept closures with
impl Fn(A) -> B(or the other closure traits, next lesson). Return them with-> impl Fn(A) -> Band amoveclosure. - Store closures of different types as
Box<dyn Fn(A) -> B>. - Named functions and methods (
String::from,f64::sqrt) can be passed wherever a closure is expected. Non-capturing closures convert tofnpointers.
Next: Fn, FnMut & FnOnce.
