19.4Iterator adapters

Last updated June 13, 2026

An iterator produces values one at a time (lesson 19.3). An adapter is a method on an iterator that returns another iterator, a transformed one. Because adapters return iterators, you can chain them: each link in the chain takes values from the link before it and hands transformed values to the link after. And because iterators are lazy, building the whole chain costs nothing; the work happens only when a consumer (next lesson) pulls values through.

Adapters are where iterator code starts to read like a description of what you want rather than a recipe of how to loop. Let's meet the ones you'll use constantly. Every example here ends with .collect() into a Vec just so we can see the result; collect is properly covered in lesson 19.5, so for now read it as "run the pipeline and gather the output into a vector."

map: transform each item

map applies a closure to every item, yielding the results:

fn main() {
    let nums = vec![1, 2, 3, 4];
    let squares: Vec<i32> = nums.iter().map(|x| x * x).collect();
    println!("{squares:?}");
}
[1, 4, 9, 16]

The closure runs once per item, and map yields whatever it returns. The output type can differ from the input type, mapping numbers to their string forms gives you an iterator of String:

let nums = vec![1, 2, 3];
let labels: Vec<String> = nums.iter().map(|n| format!("#{n}")).collect();
println!("{labels:?}");   // ["#1", "#2", "#3"]

filter: keep some items

filter takes a closure returning bool and yields only the items for which it returns true:

fn main() {
    let nums = vec![1, 2, 3, 4, 5, 6];
    let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
    println!("{evens:?}");
}
[2, 4, 6]

One papercut to flag now, because it bites everyone. The closure gets &&i32 here, a reference (from filter) to a reference (from iter), so it needs *x to reach the number for the %. Filtering and mapping references stacks up &s, and the dereferences are how you peel them. If you see a closure body sprouting *s, this is usually why; it's mechanical, not deep.

map and filter together are the bread and butter. "Square the odd numbers" is a filter then a map:

let nums = vec![1, 2, 3, 4, 5];
let odd_squares: Vec<i32> = nums.iter()
    .filter(|x| *x % 2 == 1)
    .map(|x| x * x)
    .collect();
println!("{odd_squares:?}");   // [1, 9, 25]

Read top to bottom, it's a sentence: take the numbers, keep the odd ones, square them, collect. The order matters, filtering before mapping squares only three values; the other arrangement would square all five and then filter, more work for the same answer.

enumerate: pair each item with its index

enumerate wraps each item in a tuple (index, item), counting from zero. This is the idiomatic answer to "I need the position too", far better than an external counter variable:

fn main() {
    let words = vec!["zero", "one", "two"];
    for (i, w) in words.iter().enumerate() {
        println!("{i}: {w}");
    }
}
0: zero
1: one
2: two

Notice this one drives the pipeline with a for loop instead of collect. Adapters feed consumers and for loops equally; a for over an adapter chain is completely normal.

zip: walk two iterators in lockstep

zip pairs up the items of two iterators, yielding tuples, and stops as soon as either runs out:

fn main() {
    let names = vec!["Ada", "Alan", "Grace"];
    let ages = vec![36, 41, 45];
    let paired: Vec<(&str, i32)> = names.iter().copied()
        .zip(ages.iter().copied())
        .collect();
    println!("{paired:?}");
}
[("Ada", 36), ("Alan", 41), ("Grace", 45)]

(.copied() turns an iterator of &T into one of T for Copy types, here just so the tuples hold &str and i32 rather than references; you'll meet it again next lesson.) The "stops at the shorter" rule is a feature: zipping a list with 0.. (an infinite counting iterator) is another way to get indices, pairing each item with 0, 1, 2, and the infinite side simply stops when the finite side does.

rev and chain

rev reverses an iterator's direction (it works on iterators that can be walked from both ends, which includes vectors and ranges):

let countdown: Vec<i32> = (1..=5).rev().collect();
println!("{countdown:?}");   // [5, 4, 3, 2, 1]

chain glues two iterators end to end, yielding all of the first, then all of the second:

let a = vec![1, 2];
let b = vec![3, 4];
let both: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
println!("{both:?}");   // [1, 2, 3, 4]

These six, map, filter, enumerate, zip, rev, chain, plus the consumers next lesson, cover the large majority of real iterator code. There are dozens more in the standard library (take, skip, step_by, flat_map, take_while), and they all follow the same shape: a method on an iterator returning a new iterator. Once you've internalized the pattern, learning a new adapter is reading one line of documentation.

Laziness, demonstrated

We claimed last lesson that adapters do nothing until consumed. Here's the proof, with two adapters and no consumer:

fn main() {
    let v = vec![1, 2, 3];
    let _pipeline = v.iter()
        .map(|x| { println!("mapping {x}"); x * 2 })
        .filter(|x| { println!("filtering {x}"); *x > 2 });

    println!("built the pipeline; ran nothing");
}
built the pipeline; ran nothing

Not one "mapping" or "filtering" line. The chain is fully built and then dropped, unconsumed, so zero closures run. Add a .collect::<Vec<_>>() to the end and you'd see the prints interleave per element (map 1, filter 2, map 2, filter 4, ...), because each value flows all the way through the chain before the next one starts. That per-element flow, not "all maps, then all filters", is the consequence of laziness, and it's what lets you chain a take(3) after an expensive map and have the map run only three times.

Key insight

An adapter chain is a plan, assembled lazily, that processes one element at a time when finally driven. This is why iterator pipelines are as fast as hand-written loops (lesson 19.7 measures it): there's no intermediate vector built between map and filter, each value passes through the whole chain in one go. You get the readability of "transform, then keep, then collect" with the performance of a single loop.

Quiz time

Question #1

What distinguishes an adapter from a consumer?

Show solution

An adapter (like map, filter, enumerate, zip, rev, chain) returns a new iterator, so adapters are lazy and chainable, doing no work until driven. A consumer (like collect, sum, for) actually drives the iterator by calling next repeatedly, producing a final value or side effects. A chain of adapters with no consumer at the end does nothing.

Question #2

Write a pipeline that, given vec![1, 2, 3, 4, 5, 6], produces a Vec<i32> of the squares of the even numbers.

Show solution
let nums = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = nums.iter()
    .filter(|x| *x % 2 == 0)
    .map(|x| x * x)
    .collect();
// [4, 16, 36]

Filter first (keep evens), then map (square them). Filtering before mapping does less work, since only the even values get squared. The *x in the filter peels the &&i32 down to compare; mapping x * x over &i32 works directly.

Question #3

enumerate is the clean way to get indices. How could you get the same (index, item) pairs using zip instead?

Show solution

Zip the iterator with an infinite counting range, putting the counter first: (0..).zip(v.iter()) yields (0, &v[0]), (1, &v[1]), and so on. zip stops when the shorter side ends, and v.iter() is finite, so the infinite 0.. simply stops with it. enumerate is the purpose-built, more readable version of exactly this.

Adapters build pipelines but never finish them. The next lesson (19.5) covers the consumers, collect, sum, count, fold, find, any, all, that drive a pipeline and turn it into a value.