Skip to content
AgileLogics
5 min read

Iterators

Prerequisites: Closures

An iterator produces a sequence of values one at a time. Every for loop you have written uses one. Any type implementing the Iterator trait can be iterated; the trait needs just one method:

trait Iterator {                                   // simplified from the standard library
    type Item;                                     // the type of value produced
    fn next(&mut self) -> Option<Self::Item>;      // Some(value), or None when finished
}

On top of next, the trait provides dozens of methods for free.

Three ways to iterate a collection

Method Yields The collection afterwards
v.iter() &T unchanged, still usable
v.iter_mut() &mut T still usable; items may have been modified
v.into_iter() T moved: no longer usable

for x in &v is shorthand for v.iter(), for x in &mut v for v.iter_mut(), and for x in v for v.into_iter().

Adapters and consumers

Adapters such as map, filter, enumerate, zip, take, skip, rev and chain wrap an iterator in a new one. Consumers such as collect, sum, count, fold, max, min, find, any and all pull values through and produce a result.

Iterators are lazy: adapters do nothing until a consumer asks for values. Each value flows through the whole chain before the next one starts, and no intermediate collections are created.

Syntax

fn main() {
    let v = vec![1, 2, 3, 4];
    let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
    let evens = v.iter().filter(|x| *x % 2 == 0).count();
    let total: i32 = v.iter().sum();
    println!("{doubled:?} {evens} {total}");
}

Example

struct Countdown {
    remaining: u32,
}

impl Iterator for Countdown {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.remaining == 0 {
            None
        } else {
            self.remaining -= 1;
            Some(self.remaining + 1)
        }
    }
}

fn main() {
    let prices = vec![12.5, 3.0, 8.25, 20.0];

    let total: f64 = prices.iter().sum();
    println!("total: {total}");

    let cheap: Vec<f64> = prices.iter().copied().filter(|p| *p < 10.0).collect();
    println!("cheap: {cheap:?}");

    for (i, p) in prices.iter().enumerate().skip(2) {
        println!("item {i}: {p}");
    }

    let names = ["ada", "grace", "linus"];
    let ages = [36, 85, 54];
    let people: Vec<String> = names
        .iter()
        .zip(ages.iter())
        .map(|(name, age)| format!("{name} ({age})"))
        .collect();
    println!("{}", people.join(", "));

    let mut scores = vec![1, 2, 3];
    for s in scores.iter_mut() {
        *s *= 10;
    }
    println!("scores: {scores:?}");

    let product = (1..=5).fold(1, |acc, n| acc * n);
    println!("5! = {product}");

    let first_long = names.iter().find(|n| n.len() > 4);
    println!("first long name: {first_long:?}");

    // Laziness: nothing is printed until `collect` pulls values through.
    let lazy = [1, 2, 3].iter().map(|n| {
        println!("  processing {n}");
        n * n
    });
    println!("before collect");
    let squares: Vec<i32> = lazy.collect();
    println!("squares: {squares:?}");

    // Our own iterator gets every adapter for free.
    let launch: Vec<u32> = Countdown { remaining: 5 }.filter(|n| n % 2 == 1).collect();
    println!("odd countdown: {launch:?}");
}

Expected output

total: 43.75
cheap: [3.0, 8.25]
item 2: 8.25
item 3: 20
ada (36), grace (85), linus (54)
scores: [10, 20, 30]
5! = 120
first long name: Some("grace")
before collect
  processing 1
  processing 2
  processing 3
squares: [1, 4, 9]
odd countdown: [5, 3, 1]

.copied() turns an iterator of &f64 into one of f64, which is convenient for Copy types. For others, use .cloned() or iterate with into_iter().

Common mistakes

  • Calling collect without saying what to build: let v = nums.iter().map(|x| x * 2).collect(); fails with error[E0283]: type annotations needed. Annotate the variable (let v: Vec<i32>) or use the turbofish: .collect::<Vec<_>>().
  • Building an adapter chain and never consuming it. The code does nothing and the compiler warns unused `Map` that must be used. Use a for loop for side effects.
  • Using a Vec after into_iter() (or after for x in v): error[E0382]: borrow of moved value: `v` . Iterate over &v if you need it again.
  • Collecting into a Vec only to loop over it again. Keep chaining adapters instead; it avoids allocating.
  • Summing without a type: let s = v.iter().sum(); also needs an annotation, such as let s: i32.

Practice

Given let words = ["apple", "Bob", "kayak", "Rust", "level", "noon"];, use an iterator chain to collect every palindrome (case-insensitive) into a Vec<String> in upper case, then print it and its length.

Show solution
fn main() {
    let words = ["apple", "Bob", "kayak", "Rust", "level", "noon"];

    let palindromes: Vec<String> = words
        .iter()
        .map(|w| w.to_lowercase())
        .filter(|w| w.chars().eq(w.chars().rev()))
        .map(|w| w.to_uppercase())
        .collect();

    println!("{palindromes:?} ({} found)", palindromes.len());
}
["BOB", "KAYAK", "LEVEL", "NOON"] (4 found)