Rust / the practical course

02 / Application design

Iterators and closures

Follow iterator items, closure captures, and errors through your code.

45 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

An iterator describes a sequence of steps

An iterator produces an Option<Item> each time next is called. Many adaptors, including map and filter, are lazy: constructing them does not perform the whole computation. A consumer such as collect, sum, or a for loop drives the steps.

Run the example

cargo run --locked --example 11_iterators
fn main() {
    let rows = ["INFO a", "WARN b", "WARN c"];
    let warnings: Vec<_> = rows
        .iter()
        .copied()
        .filter(|row| row.starts_with("WARN "))
        .collect();
    assert_eq!(warnings, ["WARN b", "WARN c"]);
    let parsed: Result<Vec<_>, _> = rows.iter().map(|row| audit_core::parse(row)).collect();
    assert_eq!(parsed.unwrap().len(), 3);
}

View the tested source

rows.iter() yields references to array elements, so each item is &&str. copied() copies the small shared reference to give &str; it does not duplicate the text allocation. filter itself passes a reference to its item to the predicate. Check the item type at each step so you know which references to dereference.

Collecting an iterator of Result values into Result<Vec<_>, _> stops at the first error. This chooses to stop on the first failure. Collecting all errors requires a different design and often a different output type. A concise iterator chain is good only when its policy remains clear.

Closures capture what they need

A closure is an anonymous function-like value that can capture its environment. It may borrow shared data, borrow it mutably, or consume captured values depending on its body. move requests ownership capture; it does not by itself imply the closure can only be called once. The operations performed by the body determine whether it implements Fn, FnMut, or only FnOnce.

A closure that consumes a captured string by returning it can generally run only once. A closure that owns a string but only reads its length can run repeatedly. This distinction matters when passing closures to iterators and spawning tasks.

Prefer understandable data flow

A loop is often clearer when you must update several counters, attach a line number to an error, and maintain a record-size limit. Use whichever is easier to follow. Both can compile efficiently; measure before claiming one is faster.

Avoid collecting merely to iterate immediately again when a streaming consumer would do. Conversely, collecting is sensible when you need random access or multiple passes. If a borrow makes the chain difficult to write, check who needs to own the data before cloning the collection.

Exercise

Change the fixture to include BOGUS bad between two valid records. Show that collecting into Result<Vec<_>, _> returns the parser error. Then implement an explicit loop that counts valid records and errors separately, and write down how that changes the tool's contract.

Solution and acceptance check

The first version returns UnknownLevel, with no successful vector. The second can retain two valid records and one rejection count. Report the rejected record too, so users know the counts are incomplete. Use a match on each result to make that policy visible.

Sources: Iterator, closure capture, and FromIterator for Result.

Find a lesson