Guide contents

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.

Rust
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));
}
Output
42 5 hello 7 is odd

2. 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.

Rust
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));
}
Output
13.5 4.5

3. 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.

Rust✗ does not compile · E0308
fn main() {
    let echo = |x| x;
    let s = echo(String::from("text"));
    let n = echo(5);
}
Output
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.

Rust
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}");
}
Output
12.00 EUR
119.99 EUR
still usable: EUR at 0.2

5. 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.

Rust
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}");
}
Output
clicked 1 time(s)
clicked 2 time(s)
clicked 3 time(s)
final count: 3

6. The borrow rules apply while the closure exists

While on_click holds a mutable borrow of clicks, nothing else can read it.

Rust✗ does not compile · E0502
fn main() {
    let mut clicks = 0;
    let mut on_click = || clicks += 1;
    on_click();
    println!("{clicks}");
    on_click();
}
Output
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).

Rust✗ does not compile · E0382
fn main() {
    let name = String::from("Ferris");
    let greet = move || format!("hello, {name}");
    println!("{}", greet());
    println!("{name}");
}
Output
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.

Rust
fn main() {
    let mut counter = 10;
    let mut bump = move || {
        counter += 1;
        counter
    };
    println!("{} {}", bump(), bump());
    println!("original is still {counter}");
}
Output
11 12
original is still 10

9. Closures as arguments

Standard library methods take closures everywhere. Here are the ones you have already met, plus retain and sort_by_key.

Rust
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}");
}
Output
[("cy", 45), ("ana", 120), ("dee", 300)]
2 big orders, customers ["cy", "ana", "dee"]
CY

10. 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.

Rust
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));
}
Output
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.

Rust
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);
}
Output
[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.

Rust
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"));
}
Output
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.

Rust✗ does not compile · E0373
fn make_multiplier(factor: f64) -> impl Fn(f64) -> f64 {
    |x| x * factor
}

fn main() {
    println!("{}", make_multiplier(2.0)(3.0));
}
Output
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.

Rust
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"));
}
Output
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.

Rust
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(", ") });
    }
}
Output
"Great article!": accepted
"": not empty
"Totally not a SCAM, click here": no banned words

16. 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.

Rust
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));
    }
}
Output
12 + 5 = 17
12 - 5 = 7
12 * 5 = 60
12 max 5 = 12

A 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.

Rust
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);
}
Output
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.

Rust
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}");
}
Output
listening on 8080

19. 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.

Rust
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);
}
Output
["[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.

Rust
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}");
    }
}
Output
mouse     4.8 $39
keyboard  4.5 $129
monitor   4.5 $299
webcam    3.9 $79

21. Closures and Option/Result combinators

Most combinators take closures, and they chain into readable pipelines.

Rust
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);
}
Output
discount 15%
Ok(84)

22. Realistic example: event callbacks

A tiny event emitter stores callbacks per event name. Each callback owns what it captured.

Rust
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");
}
Output
send welcome mail to ana
notify admin@example.com about ana
schedule data export for ben

impl 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.

Rust
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);
}
Output
  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.

Rust
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);
    }
}
Output
member=false coupon=None: 1 steps -> 12000
member=true  coupon=None: 2 steps -> 10800
member=true  coupon=Some(1500): 3 steps -> 9000

25. 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.

Rust
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}");
}
Output
square(4) = 16
square(-3) = 9
abs(-10) = 10
total 35

26. 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.

Rust
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);
    }
}
Output
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.

Rust
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}");
}
Output
Ada
user:3 (loaded from db)
Grace
user:4 (loaded from db)
database queries: 2

Summary

  • 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 with move.
  • 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) -> B and a move closure.
  • 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 to fn pointers.

Next: Fn, FnMut & FnOnce.