RustArena

Iterators in Rust: map, filter, collect and other adapters

How Rust iterators work: the Iterator trait, lazy adapters like map, filter and enumerate, consumers like collect, sum and fold, and writing your own.

An iterator produces a sequence of values, one at a time. Anything implementing the Iterator trait has a next method that returns Some(item) until it's done, then None. for loops call next for you.

fn main() {
    let orders = vec![120, 45, 300, 80, 15];
    let big_total: i32 = orders
        .iter()
        .filter(|&&amount| amount >= 50)
        .map(|amount| amount * 2)
        .sum();
    println!("{big_total}");

    let labels: Vec<String> = orders.iter().enumerate().map(|(i, a)| format!("#{}={a}", i + 1)).collect();
    println!("{labels:?}");
}
1000
["#1=120", "#2=45", "#3=300", "#4=80", "#5=15"]

Three ways to start

Adapters (lazy) and consumers (do the work)

Adapters take an iterator and return a new one. They're lazy: nothing runs until a consumer pulls values through. Calling v.iter().map(|x| x * 2); on its own does nothing, and rustc warns: "unused Map that must be used... iterators are lazy and do nothing unless consumed".

Adapters Consumers
map, filter, filter_map collect, sum, product, count
enumerate, zip, chain min, max, min_by_key, max_by
take, skip, take_while, skip_while fold, reduce
rev, step_by, peekable any, all, find, position
flat_map, flatten, inspect last, nth, for_each, partition
cloned, copied, windows/chunks (on slices) join (on Vec<String> after collect)

collect needs a target type

collect can build a Vec, String, HashMap, HashSet, BTreeMap and more, so tell it which: let v: Vec<_> = ...collect(); or .collect::<Vec<_>>(). Without that you get E0283 or E0282, "type annotations needed". Two handy tricks:

Zero-cost

Iterator chains compile down to the same machine code as a hand-written loop, often better, since bounds checks can be removed. Prefer them for clarity; there's no speed penalty in release builds.

Writing your own

Implement next, and every adapter above comes for free:

impl Iterator for Countdown {
    type Item = u32;
    fn next(&mut self) -> Option<u32> { ... }
}

Common mistakes

Example

use std::collections::HashMap;

struct Countdown {
    n: u32,
}

impl Iterator for Countdown {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.n == 0 {
            return None;
        }
        self.n -= 1;
        Some(self.n + 1)
    }
}

fn main() {
    let log = "INFO start\nWARN disk 91%\nERROR db down\nINFO retry\nERROR db down";

    let errors: Vec<&str> = log.lines().filter(|l| l.starts_with("ERROR")).collect();
    println!("{} errors, first: {:?}", errors.len(), errors.first());

    let counts: HashMap<&str, usize> = log.lines().filter_map(|l| l.split_whitespace().next()).fold(HashMap::new(), |mut m, level| {
        *m.entry(level).or_insert(0) += 1;
        m
    });
    let mut levels: Vec<_> = counts.into_iter().collect();
    levels.sort();
    println!("{levels:?}");

    let parsed: Result<Vec<i32>, _> = "4 8 15 16".split(' ').map(|s| s.parse::<i32>()).collect();
    let broken: Result<Vec<i32>, _> = "4 x 15".split(' ').map(|s| s.parse::<i32>()).collect();
    println!("{parsed:?} / {}", broken.is_err());

    let squares: Vec<u64> = (1..=5).map(|n: u64| n * n).collect();
    let (even, odd): (Vec<u64>, Vec<u64>) = squares.iter().partition(|&&n| n % 2 == 0);
    println!("{squares:?} even {even:?} odd {odd:?}");

    let launch: Vec<String> = Countdown { n: 3 }.map(|n| n.to_string()).chain(["liftoff".to_string()]).collect();
    println!("{}", launch.join(" "));

    let best = ["Ferris:31", "Corro:12", "Gopher:25"]
        .iter()
        .filter_map(|s| s.split_once(':'))
        .max_by_key(|(_, score)| score.parse::<u32>().unwrap_or(0));
    println!("{best:?}");
}

Output:

2 errors, first: Some("ERROR db down")
[("ERROR", 2), ("INFO", 2), ("WARN", 1)]
Ok([4, 8, 15, 16]) / true
[1, 4, 9, 16, 25] even [4, 16] odd [1, 9, 25]
3 2 1 liftoff
Some(("Ferris", "31"))

Practice it