Skip to content
rusthacks.dev
Rust

Iterators in Rust From For Loops to Adapter Chains

Iterators are lazy: adapters like map and filter do nothing until a consumer such as collect or sum runs. See the chain, the traits and the common traps.

7 min read
Summarize in AI
Flat illustration for the post “Iterators in Rust From For Loops to Adapter Chains”, composed around iterators.

A for loop tells the computer how to walk a collection. An iterator chain tells it what you want from the collection and leaves the walking to the compiler. Rust's iterators are the reason code like words.iter().filter(...).map(...).collect() is both idiomatic and fast. This post runs the main pieces, proves the laziness with a program that prints, shows the two errors beginners meet most, and implements an iterator by hand. All programs ran on Rust 1.99, edition 2024.

A first chain

src/main.rsrust
fn main() {
    let words = ["apple", "fig", "banana", "kiwi", "cherry", "date"];

    let result: Vec<String> = words
        .iter()                              // yields &&str
        .filter(|w| w.len() > 3)             // keep long words
        .map(|w| w.to_uppercase())           // transform each
        .collect();                          // consume into a Vec

    println!("{result:?}");

    let total_len: usize = words.iter().map(|w| w.len()).sum();
    println!("total length {total_len}");

    let longest = words.iter().max_by_key(|w| w.len());
    println!("{longest:?}");

    let (short, long): (Vec<&str>, Vec<&str>) = words.iter().partition(|w| w.len() <= 4);
    println!("{short:?} | {long:?}");
}
Text
["APPLE", "BANANA", "KIWI", "CHERRY", "DATE"]
total length 28
Some("cherry")
["fig", "kiwi", "date"] | ["apple", "banana", "cherry"]

The first expression is the shape to learn. words.iter() produces an iterator over references. filter keeps the words longer than three letters, map uppercases them and collect gathers the result into a Vec<String>. Each step is an adapter that takes an iterator and returns another one; the last is a consumer that actually pulls the items through. The other lines show common consumers: sum totals the lengths (28), max_by_key picks the longest word, and partition splits one pass into two vectors.

Look at the second line of output for a detail that catches people. banana and cherry both have six letters, and max_by_key returned cherry: when several elements tie for the maximum it returns the last one, while min_by_key returns the first. If ties matter, say how you want them broken.

Nothing happens until you ask

Adapters are lazy. Building a chain does no work; the work happens when a consumer pulls on the end. You can watch it:

src/main.rsrust
fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];

    let doubled = numbers.iter().map(|n| {
        println!("  mapping {n}");
        n * 2
    });
    println!("built the iterator, nothing printed yet");

    let first_big = doubled.filter(|n| *n > 6).next();
    println!("first > 6: {first_big:?}");
}
Text
built the iterator, nothing printed yet
  mapping 1
  mapping 2
  mapping 3
  mapping 4
first > 6: Some(8)

The map closure prints a line whenever it runs. The first message, "built the iterator", appears before any mapping, so creating doubled did nothing. Then filter(...).next() asks for one item that is greater than 6. The pipeline pulls 1, 2, 3 and 4 through the map, and stops at 4 doubled to 8, the first value above 6. The elements 5 and 6 were never mapped. A hand-written loop that built a full doubled vector first would have done all six.

This is why chains with find, take, any and position can be cheap on large inputs, and why iterators and loops perform the same: there is no intermediate collection between the steps, just one loop that the compiler fuses.

Three ways to iterate a vector

Whether a loop borrows, mutates or consumes depends on which method you call:

src/main.rsrust
fn main() {
    let mut names = vec![String::from("ada"), String::from("grace")];

    for n in names.iter() {                 // &String: borrow
        println!("{n}");
    }
    for n in names.iter_mut() {             // &mut String: modify in place
        n.push('!');
    }
    for n in &names {                       // same as .iter()
        print!("{n} ");
    }
    println!();
    for n in names {                        // String: consumes the vector
        print!("{} ", n.len());
    }
    println!();
}
Text
ada
grace
ada! grace! 
4 6 

names.iter() yields &String, so the loop can read but not change. names.iter_mut() yields &mut String, so n.push('!') changed the strings in place, which the next loop shows with ada! and grace!. for n in &names is shorthand for .iter(). The last loop, for n in names, calls into_iter() and takes ownership of the vector: it yields owned Strings, and names cannot be used afterwards. That is the choice behind many "value moved" errors, covered in ownership.

A related mistake is calling an adapter on the collection itself:

src/main.rsrust
fn main() {
    let v = vec![1, 2, 3];
    let doubled: Vec<i32> = v.map(|n| n * 2).collect();
    println!("{doubled:?}");
}
Text
error[E0599]: no method named `map` found for struct `Vec<{integer}>` in the current scope
 --> src/main.rs:3:31
  |
3 |     let doubled: Vec<i32> = v.map(|n| n * 2).collect();
  |                               ^^^ `Vec<{integer}>` is not an iterator
  |
help: call `.into_iter()` first
  |
3 |     let doubled: Vec<i32> = v.into_iter().map(|n| n * 2).collect();

A Vec is not an iterator, it is something you can make one from. The help text tells you to call .into_iter() first, or .iter() if you want to keep the vector.

collect needs to know what to build

collect can build many containers, so it cannot guess:

src/main.rsrust
fn main() {
    let words = ["a", "b"];
    let upper = words.iter().map(|w| w.to_uppercase()).collect();
    println!("{}", upper.len());
}
Text
error[E0282]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let upper = words.iter().map(|w| w.to_uppercase()).collect();
  |         ^^^^^
4 |     println!("{}", upper.len());
  |                    ----- type must be known at this point
  |
help: consider giving `upper` an explicit type
  |
3 |     let upper: Vec<_> = words.iter().map(|w| w.to_uppercase()).collect();
  |              ++++++++

The compiler cannot tell whether upper should be a Vec, a HashSet or a String because upper.len() works on all of them. Give it the type, and there are three ways:

src/main.rsrust
use std::collections::{BTreeMap, HashSet};

fn main() {
    let words = ["b", "a", "b", "c"];

    let v = words.iter().collect::<Vec<_>>();              // turbofish on collect
    let set: HashSet<_> = words.iter().collect();          // annotation on the binding
    let counts = words.iter().fold(BTreeMap::new(), |mut m, w| {
        *m.entry(*w).or_insert(0) += 1;
        m
    });
    let joined = words.iter().copied().collect::<Vec<_>>().join("+");
    let parsed: Result<Vec<i32>, _> = ["1", "2", "x"].iter().map(|s| s.parse::<i32>()).collect();

    println!("{v:?} {} unique", set.len());
    println!("{counts:?}");
    println!("{joined}");
    println!("{parsed:?}");
}
Text
["b", "a", "b", "c"] 3 unique
{"a": 1, "b": 2, "c": 1}
b+a+b+c
Err(ParseIntError { kind: InvalidDigit })

collect::<Vec<_>>() is the turbofish: the _ lets the compiler infer the element type while you name the container. let set: HashSet<_> = ... puts the annotation on the binding instead. fold builds a BTreeMap of counts by threading an accumulator through each element, and the Result<Vec<i32>, _> line shows the most useful collect trick: it gathers values, but stops and returns the first Err, here InvalidDigit for "x". That turns "parse every field or fail" into one expression instead of a loop with an early return. The same applies to Option.

Short-circuiting and infinite iterators

Some consumers stop early, so the iterator may never end:

src/main.rsrust
fn main() {
    let big = (1..).map(|n| n * n);                     // infinite
    let first: Vec<u64> = big.clone().take(5).collect();
    println!("{first:?}");

    let found = big.clone().find(|n| n % 7 == 0 && *n > 100);
    println!("{found:?}");
    println!("{}", big.clone().take_while(|n| *n < 50).count());
    println!("{}", [3, 5, 8].iter().any(|n| n % 2 == 0));
    println!("{}", [3, 5, 8].iter().all(|n| n % 2 == 1));
    println!("{:?}", [3, 5, 8].iter().position(|&n| n == 5));
    println!("{:?}", (1..=4).rev().collect::<Vec<_>>());
}
Text
[1, 4, 9, 16, 25]
Some(196)
7
true
false
Some(1)
[4, 3, 2, 1]

(1..) counts forever, and .map(|n| n * n) squares every number. take(5) keeps the first five squares. find stops at the first square that is divisible by 7 and above 100, which is 196. take_while(|n| *n < 50).count() counts 7 squares below 50 (1, 4, 9, 16, 25, 36, 49). The booleans from any and all, the index from position and the reversed range all stop as soon as the answer is known. The clone() calls are there because consuming an iterator uses it up; a cloneable adapter chain can be restarted.

Writing your own

The trait has one required method:

src/main.rsrust
struct Countdown(u32);

impl Iterator for Countdown {
    type Item = u32;

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

fn main() {
    let v: Vec<u32> = Countdown(5).collect();
    println!("{v:?}");

    // All adapters work on it for free.
    let evens: Vec<u32> = Countdown(10).filter(|n| n % 2 == 0).map(|n| n * n).collect();
    println!("{evens:?}");
    println!("{}", Countdown(4).zip(Countdown(100)).map(|(a, b)| a * b).sum::<u32>());
}
Text
[5, 4, 3, 2, 1]
[100, 64, 36, 16, 4]
990

Countdown implements next, returning Some until it hits zero. That is all it needs: collect, filter, map, zip and sum come with the trait. The last line zips the countdown from 4 with the countdown from 100, multiplies the pairs and sums them: 4 x 100 + 3 x 99 + 2 x 98 + 1 x 97 = 990. Zipping stopped when the shorter side ended. Implementing Iterator is the right way to expose a sequence from your own type, and IntoIterator is the trait that makes for x in my_type work.

Habits

  • Use a loop when you need break with complicated state or several mutable variables, and a chain when each step is a transformation.
  • Write the type of a collect target once, in the place that reads best.
  • Avoid collecting into a Vec in the middle of a chain just to iterate it again; pass the iterator along.
  • Use sum::<i32>() or sum::<u64>() when the compiler cannot infer the type.
  • Reach for parallel iterators only after the sequential version works, and measure.

When a loop is clearer

A long chain with nested closures and four adapters can be harder to debug than a loop: there is no line to put a println! on. If a chain stops being readable, split it into named intermediate iterators (let long_words = words.iter().filter(...);) or a helper function. And for code where you need the index and the value and the previous value together, a plain for i in 0..n or windows(2) often says it better than zip and skip.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.