R Rust by Evidence PDF

Chapter 11 — Iterators and Closures: Computation on Demand

Goal: Read iterator chains as a sequence of delayed transformations, and predict how a closure captures its environment.

Start with a program that looks as if it should print three lines:

fn main() {
    let names = ["Ada", "Linus", "Grace"];
    names.iter().map(|name| println!("hello, {name}"));
}

Prediction

What happens?

A. It prints all three greetings.
B. It prints one greeting.
C. It prints nothing and compiles quietly.
D. It prints nothing, with a warning that an iterator must be used.

RevealD

The representative warning is:

warning: unused `Map` that must be used
  = note: iterators are lazy and do nothing unless consumed

map builds a value describing future work. It does not perform that work. Iterator is centered on one method:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Adapters such as map, filter, and take wrap an iterator and implement another next. A consumer such as collect, sum, fold, or a for loop repeatedly calls next.

Working model: an iterator chain is a recipe; a consumer runs it one item at a time.

Precise model: each adapter is a concrete state machine. Rust usually monomorphizes and inlines the chain, so the convenient pipeline need not allocate an intermediate collection. Laziness is about when next is called, not about background work.

The simplest fix is to use the operation meant for side effects:

fn main() {
    let names = ["Ada", "Linus", "Grace"];
    names.iter().for_each(|name| println!("hello, {name}"));
}

A plain loop is often clearer and is equally valid:

let names = ["Ada", "Linus", "Grace"];
for name in &names {
    println!("hello, {name}");
}

Use iterator adapters when they express a value transformation. Prefer for when the body is mostly effects, has several branches, or needs break and continue.

Closures make these pipelines local. Their capture mode follows use. A closure may borrow immutably, borrow mutably, or take ownership:

fn main() {
    let mut total = 0;
    let values = [2, 4, 6];

    values.iter().for_each(|n| total += n);
    println!("{total}"); // 12
}

Because the closure mutates total, calling it needs mutable access to its captured environment; it implements FnMut. A closure that only reads captures can implement Fn. A closure that consumes a captured value may only implement FnOnce. These traits describe how the closure can be called, not merely whether the source contains move.

move forces captures by value:

let label = String::from("worker");
let show = move || println!("{label}");
show();

It does not force every captured value to be destroyed on the first call. Here printing only borrows label, so show remains callable. move is commonly needed when a closure must outlive its creating scope, such as a thread closure.

Iterator ownership follows three familiar entry points:

Mini challenge

What does this print?

fn main() {
    let words = vec![String::from("ant"), String::from("bear")];
    let lengths: Vec<usize> = words.iter().map(String::len).collect();
    println!("{} {:?}", words.len(), lengths);
}

A. 0 [3, 4]
B. 2 [3, 4]
C. It fails because map moves each String.
D. It fails because String::len cannot be a closure.

Answer: B. iter() yields &String; String::len accepts a shared borrow, so the strings remain owned by words. Replacing iter() with into_iter() consumes the vector. That alternative avoids borrowing and is useful when the old collection is no longer needed.