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.
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]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.
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).
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.
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();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.
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.
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 includedenumerate
.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
// 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"]Quick Reference
Adaptor | What it does |
|---|---|
| Transform each element |
| Keep elements where the closure returns true |
| Map + discard Nones in one step |
| Stop after n elements |
| Discard first n elements |
| Stop at first element failing predicate |
| Skip until predicate fails, then yield rest |
| Pair each element with its index (i, elem) |
| Pair elements from two iterators |
| Append one iterator after another |
| Map then flatten one level |
| Flatten one level of nested iterables |
| Inspect next element without consuming it |
| Clone each &T into T |
| Copy each &T into T (Copy types only) |
| Reverse iteration order |
| Yield every nth element |
| Repeat the iterator forever |
| 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.