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_whileconsumes the element that fails its predicate.take_while(|&x| x < 3)on[1, 2, 3, 4]yields1, 2, but the3that 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_whilewill silently eat it; usepeekable+ 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 need | Reach for |
|---|---|
| A running total / state between elements | scan |
| Look at the next element before taking it | peekable + peek() |
| Take/skip a prefix by condition | take_while / skip_while (eats the boundary!) |
| Iterate two sequences in lockstep | zip |
| Concatenate two iterators | chain |
| Every Nth element | step_by |
| Split into two groups by a predicate | partition |
| Pairs → two collections | unzip |
| Each element expands to many | flat_map |
| Stop early based on accumulator | scan 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
scanthreads running state through an iteration (replacing aforloop with an external mutable accumulator) and can stop early by returningNone.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_whileconsume the boundary element that fails the predicate; if you need that element later, usepeekableinstead, this off-by-one is the most common adapter bug.zipiterates two sequences in lockstep (stopping at the shorter, no panic),chainconcatenates,step_bystrides;.zip(v.iter().skip(1))pairs adjacent elements.partitionsplits into two groups by a predicate in one pass,unzipinvertszip, andflat_mapmaps-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.