Iterator Adapters in Rust

A complete guide to iterator adapters in Rust—map, filter, take, chain, and more—showing how to build lazy, composable data pipelines that replace explicit loops.

The standard library builds dozens of methods directly onto the Iterator trait. Many of those methods take one iterator and hand you back a different one. Rust calls these iterator adapters. An adapter does not pull values from the source on its own. It sets up a pipeline, and only when something downstream asks for the next element does the whole chain wake up and compute one step.

This lazy design means you can stack multiple adapters without paying for intermediate allocations or extra passes over the data. A chain of map, filter, and take runs in a single pass when the final consumer—a for loop, collect, or sum—triggers evaluation.

Laziness catches newcomers:

Every adapter in this document is lazy. Building a pipeline and never consuming it does nothing—no side effects, no computation. The compiler will warn about an unused iterator, but the bug is still common enough to call out early: iter.map(|x| do_something(x)) with no for or collect is dead code.

map and filter

map transforms every element by passing it through a closure, producing a new iterator whose item type can differ from the source. filter keeps only the elements for which a predicate closure returns true. Both are zero-cost abstractions—no heap allocation happens inside them, only the logic of the closure.

let nums = [1, 2, 3, 4, 5, 6];
// Double each value and keep only results > 5.
let processed: Vec<i32> = nums
    .iter()
    .map(|x| x * 2)
    .filter(|x| *x > 5)
    .collect();
assert_eq!(processed, vec![6, 8, 10, 12]);

The closures receive references when you call .iter() on a slice or Vec because that iterator yields &T. Inside map, x is &i32, so we dereference it before multiplying. Inside filter, the predicate receives &&i32 (a reference to the reference), and *x reaches the i32. This double reference is a frequent stumbling block. A common shorthand is to destructure the reference in the closure argument list: |&x| x * 2 and |&&x| x > 5. That makes the code read more directly while avoiding manual dereferences.

filter never changes the item type. map almost always changes either the type or the value. The pipeline above shows both cooperating: map changes i32 to i32 (same type, different values), then filter sifts through the results.

Closures borrow, not own:

The closures you pass to map and filter are FnMut closures, meaning they can capture and mutate outside state. But beware that you cannot move captured values out of a closure that runs more than once—the compiler will stop you with ownership errors. Prefer small, pure transformations.

A real-world pattern appears when parsing log lines: you map each line into a structured type and then filter out lines that failed to parse. That naturally leads into the next pair of adapters.

filter_map and flat_map

When a transformation might fail or yield zero or one result, filter_map is a tighter tool than map followed by filter. It applies a closure that returns Option<T>, discards the None variants, and unwraps the Some values. The resulting iterator produces T directly.

let raw = vec!["42", "not a number", "7", "", "12"];
let numbers: Vec<i32> = raw
    .into_iter()
    .filter_map(|s| s.parse::<i32>().ok())
    .collect();
assert_eq!(numbers, vec![42, 7, 12]);

Here parse returns Result<i32, _>, and .ok() turns it into Option<i32>. filter_map discards parse failures and delivers the valid integers. If you used map alone you would end up with an iterator of Result values, and you would need a separate filter plus map to extract the Ok payloads. filter_map condenses that into one step.

flat_map handles a different shape: it is for closures that return entire iterators. It maps each element to an iterator, then flattens the sequence of sequences into one continuous iterator. The mental model is “map, then concatenate.”

let sentences = vec!["hello world", "rust is fun"];
let words: Vec<&str> = sentences
    .iter()
    .flat_map(|s| s.split_whitespace())
    .collect();
assert_eq!(words, vec!["hello", "world", "rust", "is", "fun"]);

Each sentence is split into an iterator of words. flat_map stitches those sub-iterators together into a single iterator that yields every word in order. The function signature is flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F> where U: IntoIterator. That means the closure can return any type that can become an iterator—a Vec, an Option (which behaves like an iterator of 0 or 1 items), or a range.

flat_map can hide infinite loops:

If the inner iterator your closure returns is infinite, flat_map will never move on to the next outer element. Use take or other limiting adapters on the inner iterators when the data source is potentially unbounded.

A practical flat_map use: when processing a list of files, you might map each file path to a line iterator and flat_map them all into one stream of lines. Coupled with filter_map for line-by-line parsing, this becomes a clean I/O processing pipeline.

scan

scan is the adapter version of a fold that exposes intermediate accumulation steps. It holds an internal piece of state that you update on every element, and it can emit a transformed value or end the iteration early by returning None. The closure receives a mutable reference to the state and the current element.

let numbers = [1, 2, 3, 4];
let running_sum: Vec<i32> = numbers
    .iter()
    .scan(0, |acc, &x| {
        *acc += x;
        Some(*acc * 2) // yield double the running total
    })
    .collect();
assert_eq!(running_sum, vec![2, 6, 12, 20]);
// running totals: 1, 3, 6, 10 -> doubled: 2, 6, 12, 20

The state acc starts at 0, and each step adds the current number. We yield double the accumulated value but the underlying state remains the raw sum. Returning None from the closure would terminate the scan at that point without yielding a value for that step.

scan closure must return Option:

Forgetting to wrap the yield value in Some or accidentally returning None too early is a common bug. If the first call to your closure returns None, the resulting iterator is immediately exhausted.

Think of scan whenever you need a sliding window calculation that depends on all previous elements, like running sums, exponential moving averages, or a simple tokenizer that maintains a buffer.

take, take_while, skip, and skip_while

These four adapters control how many elements flow through the pipeline and where the iteration window sits.

  • take(n) yields the first n elements, then stops.
  • take_while(predicate) yields elements as long as predicate returns true, then stops completely—it does not resume.
  • skip(n) discards the first n elements and yields the rest.
  • skip_while(predicate) discards elements while predicate is true; once it returns false, the rest of the iterator passes through unchanged.
let vals = [3, 1, 4, 1, 5, 9, 2, 6];
// First 4 elements
let first_four: Vec<_> = vals.iter().take(4).copied().collect();
assert_eq!(first_four, vec![3, 1, 4, 1]);
// Drop elements until we see a value >= 5
let from_five: Vec<_> = vals
    .iter()
    .skip_while(|&&x| x < 5)
    .copied()
    .collect();
assert_eq!(from_five, vec![5, 9, 2, 6]);
// Take the leading run of odd numbers
let leading_odds: Vec<_> = vals
    .iter()
    .take_while(|&&x| x % 2 != 0)
    .copied()
    .collect();
assert_eq!(leading_odds, vec![3, 1]);

take_while stops at the first element that fails the predicate, even if later elements would pass again. That differs from filter, which would keep all matching elements anywhere in the sequence. The choice between them depends on whether you want a prefix or a subset.

A real-world skip appears when skipping CSV header rows. take_while can read lines from a TCP stream until an empty line marks the end of HTTP headers.

Correct window handling:

If your processing correctly uses take to limit an infinite iterator like 0.. or cycle, you have built a safe, finite pipeline. Combining 0.. with take(10) is idiomatic and prevents the runtime from diverging.

peekable and fuse

peekable wraps an iterator and adds a .peek() method that returns an optional reference to the next element without consuming it. The iterator must be called with .peekable() to gain this power. The returned reference borrows the Peekable wrapper mutably, so you cannot call .next() while a peek reference is live.

let chars = "ab".chars().peekable();
let mut iter = chars;
// Look ahead to decide whether to merge two tokens
if let Some(&c) = iter.peek() {
    if c == 'a' {
        iter.next(); // consume 'a'
        // now we can act differently based on the next char
    }
}

A typical use: writing a parser where you need to decide the next production rule based on the upcoming character without consuming it yet. peekable does not allow “putting back” elements—only peeking one deep. If you need to push back an arbitrary character, you must build a custom adapter (as shown in the itertools crate’s put_back).

fuse ensures that after an iterator returns None once, it will continue returning None forever. The Iterator trait itself does not guarantee this; some adapters may legally resume after None. Fuse wraps the iterator so that subsequent calls to .next() after exhaustion are fast and always None. Many combinators in the standard library automatically fuse the iterator if the underlying adapter can guarantee it, but calling .fuse() explicitly is a defensive practice when you hand an iterator to code that might call .next() again after exhaustion.

let mut iter = vec![1].into_iter().fuse();
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next(), None);
assert_eq!(iter.next(), None); // still None

Fuse is often automatic:

Adapters like map and filter propagate the fused property from the underlying iterator, so fuse() is rarely needed in simple chains. It becomes valuable when you wrap an iterator in an Option and call next on it after None as part of some external control flow.

Reversible Iterators and rev

Any iterator that implements DoubleEndedIterator can be reversed with .rev(). The adapter swaps the roles of next and next_back. Many standard iterators—Vec, slices, ranges—are double-ended. Reversed iteration works by traversing from the back of the data structure.

let nums = [10, 20, 30];
let reversed: Vec<_> = nums.iter().rev().collect();
assert_eq!(reversed, vec![&30, &20, &10]);

If you call rev() on an iterator that is not double-ended, the compiler will catch it at compile time because rev is only defined for Self: Sized + DoubleEndedIterator.

The mental model: rev does not reverse the underlying data in memory; it simply walks the structure backward. For a Vec, that is a constant-time operation because the iterator holds a pointer and an end pointer, and rev just swaps them. For a range, it also works instantly because range iteration is a simple arithmetic progression.

rev does not reverse the collection:

Newcomers sometimes expect vec.iter().rev() to mutate the original vector—it does not. The original vector remains unchanged, and the reversed order exists only in the iterator.

inspect

inspect is a debugging adapter that runs a closure on each element as it passes through, yielding the element unchanged. It exists purely for side effects like logging or counting. Because iterators are lazy, inspect only fires when the chain is consumed.

let sum: i32 = (1..=5)
    .inspect(|x| println!("before doubling: {x}"))
    .map(|x| x * 2)
    .inspect(|x| println!("after doubling: {x}"))
    .sum();
println!("sum: {sum}");

This prints each number before and after doubling, then prints the total. Without sum(), nothing would print. inspect is the go-to tool for understanding what a complex chain is doing at each stage without altering the pipeline.

If performance matters, remove inspect in production—the closure call adds overhead that may defeat optimizations like loop fusion. That said, it is far cheaper than allocating intermediate vectors for debugging.

chain, enumerate, and zip

Three adapters for combining iterators or adding indexing.

  • chain(other) stitches two iterators together end-to-end. Both must produce the same Item type.
  • enumerate() transforms each element into a tuple (index, value), with the index starting at 0.
  • zip(other) pairs elements from two iterators into tuples (a, b), stopping when the shorter iterator is exhausted.
let first = [1, 2];
let second = [3, 4, 5];
let chained: Vec<_> = first.iter().chain(second.iter()).collect();
assert_eq!(chained, vec![&1, &2, &3, &4, &5]);
let enumerated: Vec<_> = first.iter().enumerate().collect();
assert_eq!(enumerated, vec![(0, &1), (1, &2)]);
let zipped: Vec<_> = first.iter().zip(second.iter()).collect();
assert_eq!(zipped, vec![(&1, &3), (&2, &4)]);
// 5 is silently discarded

zip truncation is silent:

zip stops when either iterator runs out. The leftover elements in the longer iterator are never visited, and the compiler does not warn you. If you need to pair all elements with a default when one side is longer, consider itertools::zip_longest or pad the shorter iterator manually.

enumerate is particularly useful when you need both an element and its position, such as when you are building a mapping from index to value or tracking the line number during text processing. chain is common when concatenating results from multiple data sources without allocating a combined collection first.

Iteration without collection allocation:

When you use chain to combine two iterators instead of creating a new Vec by pushing, you avoid a heap allocation. The combined iterator uses only stack-allocated state to manage the two halves.

by_ref, cloned, and cycle

These adapters handle ownership, copying, and repetition.

  • by_ref() borrows the iterator mutably, returning &mut Self. This lets you call adapter methods on a mutable reference without moving ownership. It is essential when you want to use part of an iterator in one adapter chain and the rest in another.
  • cloned() creates an adapter that clones every element. It is available when the iterator yields &T where T: Clone. This turns an iterator of references into an iterator of owned values, which you can collect into an owned collection.
  • cycle() repeats the underlying iterator endlessly. It requires the original iterator to implement Clone so it can restart. If the original iterator is empty, cycle() will panic at the first call to next.
let mut iter = (1..=3).fuse();
// Take first element via by_ref
let first: Vec<_> = iter.by_ref().take(1).collect();
assert_eq!(first, vec![1]);
// The rest is still available
let rest: Vec<_> = iter.collect();
assert_eq!(rest, vec![2, 3]);

Without by_ref(), calling take() would consume iter, and you could not use it afterwards. The by_ref pattern is common in parsers and state machines that need to pull items conditionally and then continue.

cloned() eliminates the noisy dereferences when the closure would otherwise just dereference and call .clone(). Instead of .map(|x| x.clone()), write .cloned(). It is more readable and communicates intent.

cycle() is rarely used alone; it pairs with take() to create a repeating pattern. For example, cycling [true, false] and taking 10 yields an alternating sequence.

let switches: Vec<bool> = [true, false]
    .iter()
    .cycle()
    .take(10)
    .copied()
    .collect();
assert_eq!(switches, vec![true, false, true, false, true, false, true, false, true, false]);

Empty cycle panics:

Calling cycle() on an empty iterator ([0;0].iter().cycle()) compiles but panics at runtime. Always guard with a check or ensure the source has at least one element.

Putting adapters together

Iterator adapters are designed to be stacked. A single expression can handle filtering, transformation, windowing, and consumption, replacing nested for loops with a readable, declarative pipeline. The key benefit is not fewer characters but clearer intent: a chain of iter().filter(...).map(...).take_while(...) states what you want, while a loop forces the reader to reconstruct the intent from mutable variables and control flow.

When you build such a pipeline, remember that each adapter adds a small layer of wrapper structs. The compiler aggressively inlines and fuses these layers, so the resulting machine code often looks like the loop you would have written by hand. That is why Rust iterator chains have zero-cost reputation.

Adapters do not allocate:

All the adapters in this section—map, filter, flat_map, scan, take, skip, peekable, rev, inspect, chain, enumerate, zip, by_ref, cloned, cycle—operate without heap allocation. They only hold state on the stack. The first allocation happens when you call collect or otherwise store the results.


Summary

Iterator adapters transform one sequence into another without evaluating a single element until the final consumer pulls the trigger. This laziness lets you compose pipelines that read like a description of the data transformation rather than a recipe of manual loops and temporary vectors.

The mental model to carry forward: each adapter is a lens that shifts how the underlying iterator behaves. map changes values, filter hides some of them, take truncates the view, scan adds memory, and zip merges two streams. None of them mutate the source collection. They produce a new iterator whose type encodes the entire transformation chain at compile time.