RustIterator Adaptors

Iterator Adaptors

Iterator adaptors are methods that transform one iterator into another. They are the building blocks of Rust's data-pipeline style: chain them together to express complex transformations as a series of clear, composable steps — and pay no runtime cost for the abstraction.

What Is an Adaptor?

An adaptor takes an iterator and returns a new iterator. Like all iterators, the new one is lazy — it does nothing until consumed. You can stack as many adaptors as you like; the compiler fuses the entire chain into a single loop.

RUST
let v = vec![1, 2, 3, 4, 5];

// Adaptors: .filter() and .map() — no work yet
let pipeline = v.iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * 10);

// Consumed here — work happens now
let result: Vec<i32> = pipeline.collect();
println!("{:?}", result); // [20, 40]
Note
Adaptors return lazy iterator types (e.g. `Filter`, `Map`). You must consume the chain with a method like `.collect()`, `.sum()`, or `.for_each()` to actually run it.
map

.map(|x| ...) transforms every element by applying a closure. It is the most fundamental adaptor — use it to convert from one type to another or to compute a new value from each item.

RUST
let numbers = vec![1, 2, 3, 4, 5];

// Square each number
let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
println!("{:?}", squares); // [1, 4, 9, 16, 25]

// Convert strings to uppercase
let words = vec!["hello", "world"];
let upper: Vec<String> = words.iter().map(|w| w.to_uppercase()).collect();
println!("{:?}", upper); // ["HELLO", "WORLD"]
filter

.filter(|x| ...) keeps only the elements for which the closure returns true. The closure receives a reference to the item (&&T when iterating a Vec).

RUST
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];

let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6, 8]

// Combine with map — filter then transform
let even_squares: Vec<i32> = numbers
    .iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * x)
    .collect();
println!("{:?}", even_squares); // [4, 16, 36, 64]
filter_map

.filter_map(|x| ...) combines filtering and mapping in one step. The closure returns an Option: Some(value) to keep and transform the element, None to discard it. This is cleaner than a .filter() followed by .map() when the transformation itself can fail.

RUST
let strings = vec!["1", "two", "3", "four", "5"];

// Parse only the strings that are valid integers
let parsed: Vec<i32> = strings
    .iter()
    .filter_map(|s| s.parse::<i32>().ok())
    .collect();
println!("{:?}", parsed); // [1, 3, 5]

// Equivalent but more verbose with filter + map:
let parsed2: Vec<i32> = strings
    .iter()
    .filter(|s| s.parse::<i32>().is_ok())
    .map(|s| s.parse::<i32>().unwrap())
    .collect();
Tip
Whenever your `.map()` closure returns an `Option` and you want to discard the `None` values, reach for `.filter_map()` instead.
take and skip

.take(n) yields only the first n elements. .skip(n) discards the first n elements and yields the rest. Both are essential for pagination and working with infinite iterators.

RUST
let v = vec![10, 20, 30, 40, 50, 60];

let first_three: Vec<&i32> = v.iter().take(3).collect();
println!("{:?}", first_three); // [10, 20, 30]

let after_two: Vec<&i32> = v.iter().skip(2).collect();
println!("{:?}", after_two); // [30, 40, 50, 60]

// Pagination: page 2, 3 items per page
let page_size = 3;
let page_num = 1; // 0-indexed
let page: Vec<&i32> = v.iter()
    .skip(page_num * page_size)
    .take(page_size)
    .collect();
println!("{:?}", page); // [40, 50, 60]
take_while and skip_while

.take_while(|x| ...) yields elements as long as the predicate holds, then stops (even if later elements would also match). .skip_while(|x| ...) drops elements while the predicate holds, then yields all remaining elements.

RUST
let v = vec![1, 2, 3, 4, 5, 1, 2];

// Stop as soon as an element >= 4 is seen
let small: Vec<&i32> = v.iter().take_while(|&&x| x < 4).collect();
println!("{:?}", small); // [1, 2, 3]

// Skip elements < 4, then yield everything else
let rest: Vec<&i32> = v.iter().skip_while(|&&x| x < 4).collect();
println!("{:?}", rest); // [4, 5, 1, 2]  — note the trailing 1, 2 are included
Warning
`take_while` and `skip_while` are positional, not filtering — they act on the *first contiguous run* matching the predicate and then stop checking. The trailing `1, 2` in the `skip_while` example above are included because the predicate already stopped after `4` was encountered.
enumerate

.enumerate() wraps each element in a tuple (index, element) where the index starts at 0. It is the idiomatic way to get both the position and value in an iterator chain.

RUST
let fruits = vec!["apple", "banana", "cherry"];

for (i, fruit) in fruits.iter().enumerate() {
    println!("{}: {}", i, fruit);
}
// 0: apple
// 1: banana
// 2: cherry

// Useful inside map to include position in output
let labelled: Vec<String> = fruits
    .iter()
    .enumerate()
    .map(|(i, &f)| format!("{}. {}", i + 1, f))
    .collect();
println!("{:?}", labelled); // ["1. apple", "2. banana", "3. cherry"]
zip

.zip(other) pairs up elements from two iterators into tuples (a, b). It stops as soon as either iterator is exhausted, so the output length equals the shorter one.

RUST
let names = vec!["Alice", "Bob", "Carol"];
let scores = vec![95, 87, 73, 100]; // longer — the 100 is dropped

let combined: Vec<(&&str, &i32)> = names.iter().zip(scores.iter()).collect();
for (name, score) in &combined {
    println!("{}: {}", name, score);
}
// Alice: 95
// Bob: 87
// Carol: 73

// zip two owned iterators together
let keys = vec!["a", "b", "c"];
let values = vec![1, 2, 3];
let map: std::collections::HashMap<&str, i32> =
    keys.into_iter().zip(values.into_iter()).collect();
println!("{:?}", map);
chain

.chain(other) concatenates two iterators end-to-end, yielding all elements from the first followed by all elements from the second.

RUST
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];

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

// Chain more than two iterators
let c = vec![7, 8];
let all: Vec<&i32> = a.iter().chain(b.iter()).chain(c.iter()).collect();
println!("{:?}", all); // [1, 2, 3, 4, 5, 6, 7, 8]
flat_map and flatten

.flat_map(|x| ...) applies a closure that returns an iterator and then flattens one level of nesting. It is equivalent to .map(...).flatten(). Use it when each element expands into multiple values.

RUST
let sentences = vec!["hello world", "foo bar baz"];

// Split each sentence into words, yielding a flat stream of words
let words: Vec<&str> = sentences
    .iter()
    .flat_map(|s| s.split_whitespace())
    .collect();
println!("{:?}", words); // ["hello", "world", "foo", "bar", "baz"]

// flatten() removes one level of nesting from an iterator of iterables
let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
let flat: Vec<&i32> = nested.iter().flatten().collect();
println!("{:?}", flat); // [1, 2, 3, 4, 5]
peekable

.peekable() wraps an iterator so you can inspect the next element without consuming it using .peek(). This is useful when parsing or when the decision to advance depends on what comes next.

RUST
let v = vec![1, 2, 3, 4, 5];
let mut iter = v.iter().peekable();

// Look ahead without consuming
if let Some(&&first) = iter.peek() {
    println!("First element is: {}", first); // 1
}

// The iterator has not advanced — next() still returns 1
println!("next() = {:?}", iter.next()); // Some(1)
println!("next() = {:?}", iter.next()); // Some(2)
cloned and copied

.iter() yields references (&T). When you need owned values, .cloned() calls .clone() on each item and .copied() does a bitwise copy (only works for types that implement Copy). Use .copied() for primitives — it is slightly cheaper.

RUST
let refs = vec![1, 2, 3, 4, 5];

// .copied() for Copy types (i32, f64, char, …)
let owned: Vec<i32> = refs.iter().copied().collect();
println!("{:?}", owned); // [1, 2, 3, 4, 5]

// .cloned() for Clone types (String, Vec, …)
let strings = vec![String::from("a"), String::from("b")];
let cloned: Vec<String> = strings.iter().cloned().collect();
// strings is still usable here
println!("{:?}", strings);
println!("{:?}", cloned);
rev

.rev() reverses the direction of iteration. It requires the underlying iterator to implement DoubleEndedIterator, which most standard-library collections do.

RUST
let v = vec![1, 2, 3, 4, 5];

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

// Useful for reading files bottom-to-top, reversing ranges, etc.
let countdown: Vec<i32> = (1..=5).rev().collect();
println!("{:?}", countdown); // [5, 4, 3, 2, 1]
step_by

.step_by(n) yields every nth element, starting with the first. It is useful for sampling sequences or simulating stride-based array access.

RUST
let v: Vec<i32> = (0..20).step_by(3).collect();
println!("{:?}", v); // [0, 3, 6, 9, 12, 15, 18]

// Every other element
let evens: Vec<i32> = (0..10).step_by(2).collect();
println!("{:?}", evens); // [0, 2, 4, 6, 8]
cycle

.cycle() repeats the iterator indefinitely — when it is exhausted, it starts again from the beginning. Always pair it with .take(n) or another terminator, otherwise the iterator is infinite.

RUST
let pattern = vec!["red", "green", "blue"];

// Repeat the colour pattern 8 times
let colours: Vec<&str> = pattern.iter().copied().cycle().take(8).collect();
println!("{:?}", colours);
// ["red", "green", "blue", "red", "green", "blue", "red", "green"]

// Stripe rows of a table with alternating labels
let labels = vec!["odd", "even"];
let rows: Vec<&str> = labels.iter().copied().cycle().take(6).collect();
println!("{:?}", rows); // ["odd", "even", "odd", "even", "odd", "even"]
scan

.scan(initial_state, |state, x| ...) is like .map() but with a persistent mutable state that is threaded through each call. The closure can return Some(value) to emit an item or None to terminate the iterator early.

RUST
let numbers = vec![1, 2, 3, 4, 5];

// Emit a running total at each step
let running_sum: Vec<i32> = numbers
    .iter()
    .scan(0, |acc, &x| {
        *acc += x;
        Some(*acc)
    })
    .collect();
println!("{:?}", running_sum); // [1, 3, 6, 10, 15]

// Stop scanning when the running sum exceeds 8
let capped: Vec<i32> = numbers
    .iter()
    .scan(0, |acc, &x| {
        *acc += x;
        if *acc > 8 { None } else { Some(*acc) }
    })
    .collect();
println!("{:?}", capped); // [1, 3, 6]
Building a Data Pipeline

The real power of adaptors comes from chaining them. Each adaptor adds one transformation to the pipeline, making the intent clear at every step.

RUST
struct Student {
    name: String,
    grade: u8,
    score: f64,
}

fn top_scorers(students: &[Student]) -> Vec<String> {
    students
        .iter()
        // Keep only passing students (score >= 60)
        .filter(|s| s.score >= 60.0)
        // Keep only senior-year students (grade 12)
        .filter(|s| s.grade == 12)
        // Sort by score descending — collect first, then sort
        // (adaptors alone cannot sort; sorting requires all elements)
        .map(|s| (s.score, &s.name))
        .collect::<Vec<_>>()
        .into_iter()
        .enumerate()
        // Take the top 3
        .take(3)
        .map(|(rank, (score, name))| {
            format!("{}. {} — {:.1}", rank + 1, name, score)
        })
        .collect()
}
Real Example: Word Frequency Counter

Here is a complete word-frequency counter built entirely from a pipeline of iterator adaptors — no explicit loops needed.

RUST
use std::collections::HashMap;

fn word_frequency(text: &str) -> Vec<(String, usize)> {
    // 1. Split into words
    // 2. Normalise to lowercase
    // 3. Remove empty strings
    // 4. Count occurrences in a HashMap
    let mut freq: HashMap<String, usize> = text
        .split_whitespace()
        .map(|w| {
            // Strip common punctuation
            w.trim_matches(|c: char| !c.is_alphabetic())
             .to_lowercase()
        })
        .filter(|w| !w.is_empty())
        .fold(HashMap::new(), |mut map, word| {
            *map.entry(word).or_insert(0) += 1;
            map
        });

    // 5. Collect into a Vec, sort by frequency (descending), then alphabetically
    let mut result: Vec<(String, usize)> = freq.into_iter().collect();
    result.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
    result
}

fn main() {
    let text = "the quick brown fox jumps over the lazy dog the fox";
    let freq = word_frequency(text);
    for (word, count) in &freq {
        println!("{}: {}", word, count);
    }
}
Performance: All Adaptors Are Lazy and Fused

Every adaptor in the standard library is lazy. When you write a chain like .filter(...).map(...).take(...), the compiler fuses all three into a single loop with no intermediate allocations. The optimizer can even apply SIMD auto-vectorisation across the fused loop.

RUST
// This entire pipeline allocates only the final Vec — nothing in between
let result: Vec<String> = (0u64..1_000_000)
    .filter(|x| x % 3 == 0)
    .map(|x| x * x)
    .take(10)
    .map(|x| x.to_string())
    .collect();

println!("{:?}", result);
// ["0", "9", "36", "81", "144", "225", "324", "441", "576", "729"]
Success
The chain above processes elements one by one through the pipeline. As soon as `.take(10)` has seen 10 items, iteration stops — the remaining 999,990 numbers are never touched.
Quick Reference

Adaptor

What it does

.map(|x| ...)

Transform each element

.filter(|x| ...)

Keep elements where the closure returns true

.filter_map(|x| ...)

Map + discard Nones in one step

.take(n)

Stop after n elements

.skip(n)

Discard first n elements

.take_while(|x| ...)

Stop at first element failing predicate

.skip_while(|x| ...)

Skip until predicate fails, then yield rest

.enumerate()

Pair each element with its index (i, elem)

.zip(other)

Pair elements from two iterators

.chain(other)

Append one iterator after another

.flat_map(|x| ...)

Map then flatten one level

.flatten()

Flatten one level of nested iterables

.peekable()

Inspect next element without consuming it

.cloned()

Clone each &T into T

.copied()

Copy each &T into T (Copy types only)

.rev()

Reverse iteration order

.step_by(n)

Yield every nth element

.cycle()

Repeat the iterator forever

.scan(state, |s, x| ...)

Stateful map with optional early termination

Summary

Iterator adaptors are the core of idiomatic Rust data processing. Each one adds a single, well-named transformation to a pipeline — and because they are all lazy, chaining ten of them costs nothing compared to chaining one. The compiler sees the entire pipeline and produces a tight, fused loop. Learn .map(), .filter(), .flat_map(), .enumerate(), and .zip() first — they cover the majority of real-world use cases — then reach for the rest as the need arises.