Closures and Iterators · about 40 min

Closures and Capturing

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

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:

  1. by shared reference &T, if it only reads the variable,
  2. by mutable reference &mut T, if it modifies it,
  3. 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

Recap

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

Exercises

Exercise 1 · write code

Sorting with closures

Read lines name age from stdin into a Vec<(String, u32)>. Print the people sorted by age (youngest first), ties broken by name, using sort_by with a closure; then print the names of everyone at least as old as a threshold read from the first line, using filter with a closure that captures the threshold.

For the input

30
ada 36
bob 25
cy 36
dee 30

print

bob 25
dee 30
ada 36
cy 36
at least 30: dee ada cy

File: main.rs

Starting point

use std::io;

fn main() {
    let mut lines = io::stdin().lines();
    let threshold: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    let mut people: Vec<(String, u32)> = Vec::new();
    for line in lines {
        let line = line.unwrap();
        let (name, age) = line.trim().split_once(' ').unwrap();
        people.push((name.to_string(), age.parse().unwrap()));
    }
}

Example

input

30
ada 36
bob 25
cy 36
dee 30

expected output

bob 25
dee 30
ada 36
cy 36
at least 30: dee ada cy
Hint 1

people.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0))) compares ages, then names.

Hint 2

people.iter().filter(|p| p.1 >= threshold) uses the captured threshold; .map(|p| p.0.as_str()) picks names.

Reference solution
use std::io;

fn main() {
    let mut lines = io::stdin().lines();
    let threshold: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    let mut people: Vec<(String, u32)> = Vec::new();
    for line in lines {
        let line = line.unwrap();
        let (name, age) = line.trim().split_once(' ').unwrap();
        people.push((name.to_string(), age.parse().unwrap()));
    }
    people.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
    for (name, age) in &people {
        println!("{name} {age}");
    }
    let old: Vec<&str> = people
        .iter()
        .filter(|p| p.1 >= threshold)
        .map(|p| p.0.as_str())
        .collect();
    println!("at least {threshold}: {}", old.join(" "));
}

Exercise 2 · write code

A closure with state

Write fn make_counter(step: u32) -> impl FnMut() -> u32 that returns a closure; each call returns the next multiple of step, starting at step. The given main prints:

5 10 15
1 2

File: main.rs

Starting point

fn main() {
    let mut by5 = make_counter(5);
    let mut by1 = make_counter(1);
    println!("{} {} {}", by5(), by5(), by5());
    println!("{} {}", by1(), by1());
}

Example

expected output

5 10 15
1 2
Hint 1

Create let mut count = 0; inside make_counter, then return a closure that changes it.

Hint 2

The closure must own count and step (they die when make_counter returns): write move || { count += step; count }.

Reference solution
fn make_counter(step: u32) -> impl FnMut() -> u32 {
    let mut count = 0;
    move || {
        count += step;
        count
    }
}

fn main() {
    let mut by5 = make_counter(5);
    let mut by1 = make_counter(1);
    println!("{} {} {}", by5(), by5(), by5());
    println!("{} {}", by1(), by1());
}

Exercise 3 · quiz

How is it captured?

let mut log = Vec::new();
let mut record = |x: i32| log.push(x);
record(1);
record(2);
println!("{:?}", log);

How does the closure capture log, and does this compile?

Answer
  • By mutable reference; it compiles, because the closure's last use is before the println!

A closure captures each variable in the least powerful way its body needs: push needs &mut log, so it borrows mutably. The borrow lasts as long as the closure is used (non-lexical lifetimes), so after the last call to record, log can be read again. Writing move would have moved log into the closure instead.