Iterator Adapters Deep Dive

Iterators Over Loops covered the everyday adapters, map, filter, collect, fold. They handle most cases, but the standard library ships a much richer set, and reaching for the right one turns code that would need a manual loop with mutable state into a single readable chain. This tutorial covers the adapters that come up once you're past the basics: stateful ones (scan, peekable, take_while), combining ones (zip, chain, step_by), and the splitters (partition, unzip). The theme is recognizing which loop a given adapter replaces.


Stateful Adapters: Carrying Information Between Elements

map and filter treat each element independently. When the transformation depends on what came before, the naive reach is a for loop with a mutable accumulator outside it. scan is the adapter for exactly that, it threads a running state through the iteration and yields a value per step:

// naive: external mutable state, manual loop
let mut running = 0;
let mut sums = Vec::new();
for x in [1, 2, 3, 4] {
    running += x;
    sums.push(running);
}   // [1, 3, 6, 10]

// idiomatic: scan carries the state internally
let sums: Vec<i32> = [1, 2, 3, 4]
    .iter()
    .scan(0, |acc, &x| { *acc += x; Some(*acc) })
    .collect();   // [1, 3, 6, 10]

scan(initial, |state, item| ...) owns the running state, and returning Some(v) yields v while None ends iteration early, so it doubles as a "stop when the accumulator hits a condition" tool. The win over the manual loop is that the state can't leak: it lives inside the chain and is gone when the chain ends, rather than sitting as a mutable variable in the surrounding scope.


peekable: Looking Without Consuming

An iterator's next() consumes the element it returns, sometimes you need to look at the next element to decide whether to take it. peekable() wraps an iterator with a .peek() method that returns a reference to the next item without advancing:

let mut iter = [1, 1, 2, 3, 3, 3].iter().peekable();
let mut runs = Vec::new();

while let Some(&x) = iter.next() {
    let mut count = 1;
    while iter.peek() == Some(&&x) {   // look ahead without consuming
        iter.next();                    // now consume the duplicate
        count += 1;
    }
    runs.push((x, count));
}   // [(1, 2), (2, 1), (3, 3)] — run-length encoding

peek() is what makes parsing-style logic ("consume while the next token matches") expressible, you decide based on the upcoming element, then choose whether to advance. Without it you'd have to consume and then awkwardly "put back" the element, which iterators don't support.

Gotcha: take_while consumes the element that fails its predicate. take_while(|&x| x < 3) on [1, 2, 3, 4] yields 1, 2, but the 3 that stopped it is gone from the iterator, not left for the next stage. If you're chaining and expect the boundary element to survive into a later step, take_while will silently eat it; use peekable + a manual loop when you need the stopping element to remain. This off-by-one surprise is the most common iterator-adapter bug.


Combining Iterators: zip, chain, step_by

Three adapters combine or stride over sequences instead of transforming single elements:

let names = ["alice", "bob", "carol"];
let scores = [85, 92, 78];

// zip: pair two iterators element-wise; stops at the shorter one
let paired: Vec<_> = names.iter().zip(scores.iter()).collect();
// [("alice", 85), ("bob", 92), ("carol", 78)]

// chain: run one iterator then another, as a single sequence
let all: Vec<i32> = [1, 2].iter().chain([3, 4].iter()).copied().collect();
// [1, 2, 3, 4]

// step_by: take every Nth element
let evens: Vec<i32> = (0..10).step_by(2).collect();
// [0, 2, 4, 6, 8]

zip is the idiomatic way to iterate two collections in lockstep, far cleaner than indexing both with a shared counter, and it stops automatically at the shorter, so there's no length-mismatch panic. A useful trick: .zip(0..) is an alternative to .enumerate(), and zipping a slice with its own tail (v.iter().zip(v.iter().skip(1))) pairs adjacent elements (the same job as windows(2) from Slices, Chunks, and Windows).


Splitting a Stream: partition, unzip, flat_map

Some adapters take one iterator and produce two collections, or restructure the shape entirely. partition splits by a predicate into two groups; unzip is the inverse of zip:

// partition: split into matches / non-matches in one pass
let (passing, failing): (Vec<i32>, Vec<i32>) =
    [45, 82, 91, 30].into_iter().partition(|&s| s >= 60);
// passing: [82, 91], failing: [45, 30]

// unzip: turn an iterator of pairs into two collections
let (names, scores): (Vec<&str>, Vec<i32>) =
    [("a", 1), ("b", 2)].into_iter().unzip();

// flat_map: map each element to an iterator, then flatten
let words: Vec<&str> = ["a b", "c d"].iter().flat_map(|s| s.split(' ')).collect();
// ["a", "b", "c", "d"]

partition does in one pass what would otherwise be two filter calls (one for each side), and it makes the "split these into two buckets" intent explicit. flat_map is map followed by flatten, the tool whenever each input element expands into zero-or-more outputs.


Adapter Cheat Sheet

You needReach for
A running total / state between elementsscan
Look at the next element before taking itpeekable + peek()
Take/skip a prefix by conditiontake_while / skip_while (eats the boundary!)
Iterate two sequences in lockstepzip
Concatenate two iteratorschain
Every Nth elementstep_by
Split into two groups by a predicatepartition
Pairs → two collectionsunzip
Each element expands to manyflat_map
Stop early based on accumulatorscan returning None, or take_while

The throughline from the original iterators tutorial holds: each of these is lazy (nothing runs until a consumer like collect/sum/for_each), and the optimizer fuses the chain into a single pass with no intermediate allocations. Reaching for the named adapter over a manual loop keeps that fusion and states the intent, the loop with external mutable state is exactly what scan, partition, and friends exist to eliminate.


Key Takeaways

  • scan threads running state through an iteration (replacing a for loop with an external mutable accumulator) and can stop early by returning None.
  • peekable() adds .peek() to look at the next element without consuming it, the enabler for parsing-style "consume while it matches" logic and run-length grouping.
  • take_while/skip_while consume the boundary element that fails the predicate; if you need that element later, use peekable instead, this off-by-one is the most common adapter bug.
  • zip iterates two sequences in lockstep (stopping at the shorter, no panic), chain concatenates, step_by strides; .zip(v.iter().skip(1)) pairs adjacent elements.
  • partition splits into two groups by a predicate in one pass, unzip inverts zip, and flat_map maps-then-flattens when each element expands into many.
  • All adapters stay lazy and fuse into a single allocation-free pass; pick the named adapter over a manual stateful loop to keep both the performance and the intent.