Rust Iterator Adapters
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5), clippy has lints for needless collect and unused iterators, but a side-effecting .map() that's never consumed can compile and silently do nothing — leaning toward d5 since clippy flags many but not all cases.
Closest to 'simple parameterised fix' (e3), per quick_fix the correction is adding a terminal operation, reordering filter before map, or removing intermediate collect — small localised pattern changes within one pipeline.
Closest to 'localised tax' (b3), iterator chains apply across all contexts (web/cli/queue/library) but each chain is self-contained; the choice does not structurally shape the system, the cost is per-pipeline.
Closest to 'serious trap' (t7), the misconception that .map()/.filter() execute immediately contradicts eager semantics in most other languages' collection methods, so a competent developer coming from elsewhere will reliably guess wrong about laziness.
Also Known As
TL;DR
Explanation
An iterator adapter is a method on the `Iterator` trait that takes an iterator and returns a new iterator wrapping it, such as `map`, `filter`, `take`, `skip`, `enumerate`, `zip`, and `flat_map`. Adapters are the building blocks of Rust's iterator pipelines, letting you express data transformations declaratively while the compiler fuses them into tight, allocation-free loops.
The single most important property of adapters is laziness. Calling `.map(...)` or `.filter(...)` does no work and touches no elements; it only constructs a new iterator type that remembers the operation. Nothing happens until a consuming method - a terminal operation such as `collect`, `sum`, `for_each`, `count`, `find`, or a `for` loop - pulls elements through the chain one at a time. This means a pipeline of ten adapters still walks the source once, pulling each element through every stage before moving to the next.
Because adapters are lazy and each returns a distinct concrete type (`Map<...>`, `Filter<...>`), the compiler can monomorphize and inline the whole chain, producing code as fast as a hand-written loop with no intermediate collections. This is why idiomatic Rust favors `iter().filter(...).map(...).collect()` over manual index loops: it is both clearer and equally fast.
Key adapters fall into groups. Transformers like `map` and `scan` reshape each item. Selectors like `filter`, `take_while`, and `skip_while` decide what passes. Combiners like `zip`, `chain`, and `flatten` merge sources. `enumerate` pairs items with their index. Adapters compose freely and most are themselves iterators, so order matters: `filter` before `map` avoids transforming items you will discard.
Common pitfalls stem from forgetting laziness. A `map` whose closure has side effects will never run if the iterator is never consumed - the compiler even warns that iterators are `#[must_use]`. Calling `collect` repeatedly allocates intermediate `Vec`s that defeat fusion. And consuming adapters by value vs borrowing via `iter`, `iter_mut`, or `into_iter` changes ownership semantics. Mastering adapters means internalizing that the chain is a recipe, not a result, until a terminal operation runs it.
Common Misconception
Why It Matters
Common Mistakes
- Calling .map() with a side-effecting closure but never consuming the iterator, so the closure never runs.
- Inserting an intermediate .collect::<Vec<_>>() between adapters, defeating fusion and adding heap allocations.
- Ordering .map() before .filter() so you transform elements you immediately discard.
- Assuming .collect() infers the target type — forgetting the turbofish or type annotation, causing a 'type annotations needed' error.
- Confusing iter(), iter_mut(), and into_iter(), borrowing when you meant to consume or vice versa.
Avoid When
- You genuinely need an intermediate collection materialized because it is consumed multiple times or its length is required mid-pipeline.
- The transformation is trivial and a plain loop reads more clearly to your team than a long adapter chain.
- You must short-circuit with complex early-exit logic that adapters express awkwardly compared to an explicit loop.
When To Use
- Expressing data transformations declaratively as map/filter/collect pipelines that fuse into allocation-free loops.
- Processing large or streaming sequences lazily without building intermediate collections.
- Composing reusable transformation steps where order can be tuned (filter before map) for efficiency.
- Replacing manual index loops with clearer, equally fast iterator chains.
Code Examples
fn main() {
let names = vec!["alice", "bob", "carol"];
// Side-effecting map that is never consumed: nothing prints.
names.iter().map(|n| {
println!("processing {}", n);
n.to_uppercase()
});
// Wasteful: map runs on every element, then filter throws most away,
// and an intermediate Vec is allocated for no reason.
let nums = vec![1, 2, 3, 4, 5, 6];
let collected: Vec<i32> = nums.iter().map(|x| x * x).collect();
let evens: Vec<i32> = collected.into_iter().filter(|x| x % 2 == 0).collect();
println!("{:?}", evens);
}
// Consume the iterator with collect so the closure (and its side effects) actually run.