Iterators
The Iterator trait is one of Rust’s most distinctive abstractions. Any type implementing Iterator admits the substantial adapter and consumer surface of the standard library: map, filter, enumerate, zip, take, skip, collect, sum, count, fold, for_each, and so on. The principal property is laziness — adapters produce new iterators without doing work; the work happens at the consumer (collect, sum, for_each, etc.). The combination — explicit-iterator-based loops, lazy adapter chains, eager consumers, generic over the item type — admits substantial conciseness with zero runtime overhead (compile-time monomorphisation produces machine code equivalent to hand-written loops).
This page covers the iterator surface, the principal adapters and consumers, and the conventional patterns.
The Iterator trait
The trait, in essence:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... many provided methods (map, filter, etc.) ...
}
The Item is an associated type; next() returns the next item or None when exhausted. The other methods (map, filter, etc.) have default implementations on top of next.
A simple iterator:
struct Counter { count: u32 }
impl Counter {
fn new() -> Self { Counter { count: 0 } }
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
let mut c = Counter::new();
println!("{:?}", c.next()); // Some(1)
println!("{:?}", c.next()); // Some(2)
Once Iterator is implemented, all the adapter and consumer methods become available:
let sum: u32 = Counter::new().sum(); // 15
let doubled: Vec<u32> = Counter::new().map(|n| n * 2).collect();
Producing iterators
The conventional sources:
let v = vec![1, 2, 3, 4, 5];
v.iter(); // iter over &T
v.iter_mut(); // iter over &mut T
v.into_iter(); // iter over T (consumes)
(0..10); // range
(0..=10); // inclusive range
"hello".chars(); // iterator over chars
"hello".bytes(); // iterator over bytes
"hello world".split_whitespace(); // iterator over &str
std::iter::repeat(5); // infinite: 5, 5, 5, ...
std::iter::once(42); // single item
std::iter::empty::<i32>(); // no items
The conventional discipline is to use iter() for borrowed iteration, iter_mut() for mutable iteration, into_iter() for consumption.
Adapters (lazy)
Adapters consume an iterator and produce another. They do no work until a consumer triggers iteration.
map
Transform each item:
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|&x| x * 2).collect(); // [2, 4, 6]
filter
Keep only items matching a predicate:
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<i32> = v.iter().filter(|&&x| x % 2 == 0).copied().collect(); // [2, 4]
filter_map
Combine filter and map; the closure returns Option<U>:
let v = vec!["1", "two", "3", "four", "5"];
let nums: Vec<i32> = v.iter().filter_map(|s| s.parse().ok()).collect(); // [1, 3, 5]
enumerate
Pair each item with its index:
for (i, x) in v.iter().enumerate() {
println!("[{}] = {}", i, x);
}
zip
Pair items from two iterators:
let a = vec![1, 2, 3];
let b = vec!["a", "b", "c"];
let pairs: Vec<(i32, &&str)> = a.iter().zip(b.iter()).collect();
// [(1, "a"), (2, "b"), (3, "c")]
The shorter iterator determines the length.
chain
Concatenate two iterators:
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
// [1, 2, 3, 4, 5, 6]
take and skip
let v = vec![1, 2, 3, 4, 5];
let first_three: Vec<i32> = v.iter().take(3).copied().collect(); // [1, 2, 3]
let after_two: Vec<i32> = v.iter().skip(2).copied().collect(); // [3, 4, 5]
take_while and skip_while
Conditional versions:
let v = vec![1, 2, 3, 4, 5, 4, 3];
let prefix: Vec<i32> = v.iter().take_while(|&&x| x < 5).copied().collect();
// [1, 2, 3, 4]
let suffix: Vec<i32> = v.iter().skip_while(|&&x| x < 4).copied().collect();
// [4, 5, 4, 3]
step_by
Every nth item:
let v: Vec<i32> = (1..=10).step_by(2).collect(); // [1, 3, 5, 7, 9]
rev
Reverse the iterator (requires DoubleEndedIterator):
let v: Vec<i32> = (1..=5).rev().collect(); // [5, 4, 3, 2, 1]
flatten and flat_map
Flatten nested iterators:
let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
let flat: Vec<i32> = nested.iter().flatten().copied().collect();
// [1, 2, 3, 4, 5]
// flat_map = map + flatten:
let words = vec!["hello", "world"];
let chars: Vec<char> = words.iter().flat_map(|s| s.chars()).collect();
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
peekable
Admits looking at the next item without consuming:
let mut iter = vec![1, 2, 3].into_iter().peekable();
if let Some(&first) = iter.peek() {
println!("first is {}", first);
}
println!("{:?}", iter.next()); // Some(1)
inspect
Inspect items without modifying — useful for debugging:
let v: Vec<i32> = (1..=5)
.inspect(|x| println!("before filter: {}", x))
.filter(|&x| x % 2 == 0)
.inspect(|x| println!("after filter: {}", x))
.collect();
Other notable adapters
.cycle(); // repeat indefinitely
.fuse(); // None forever after the first None
.cloned(); // &T → T (requires Clone)
.copied(); // &T → T (requires Copy)
.windows(n); // overlapping n-tuples (slices only)
.chunks(n); // non-overlapping chunks (slices only)
Consumers (eager)
Consumers drive the iteration; they trigger the work that adapters described.
collect
Build a collection:
let v: Vec<i32> = (1..=10).collect();
let s: String = "hello".chars().rev().collect();
let m: HashMap<i32, i32> = (1..=10).map(|n| (n, n * n)).collect();
let set: HashSet<i32> = vec![1, 2, 2, 3, 3, 3].into_iter().collect();
The target type must implement FromIterator<T> for the iterator’s item type. The turbofish ::<> admits explicit type specification:
let v = (1..=10).collect::<Vec<i32>>();
sum, product
Aggregate to a single value:
let total: i32 = (1..=10).sum(); // 55
let factorial: i32 = (1..=5).product(); // 120
count, min, max
let n = v.iter().count(); // length
let lo = v.iter().min(); // Option<&T>
let hi = v.iter().max(); // Option<&T>
min_by, max_by, min_by_key, max_by_key
let v = vec!["apple", "banana", "kiwi"];
let longest = v.iter().max_by_key(|s| s.len()); // Some(&"banana")
let shortest = v.iter().min_by_key(|s| s.len()); // Some(&"kiwi")
fold
Generic accumulation:
let sum = (1..=10).fold(0, |acc, x| acc + x); // 55
let max = vec![3, 1, 4, 1, 5, 9].into_iter().fold(0, |a, b| a.max(b));
reduce
Like fold but uses the first item as the accumulator:
let max = vec![3, 1, 4, 1, 5, 9].into_iter().reduce(|a, b| a.max(b));
// Some(9)
Returns None for empty iterators.
any, all
Boolean tests:
let v = vec![1, 2, 3, 4, 5];
let has_even = v.iter().any(|&x| x % 2 == 0); // true
let all_positive = v.iter().all(|&x| x > 0); // true
find, position
Find an item:
let v = vec![1, 2, 3, 4, 5];
let first_even = v.iter().find(|&&x| x % 2 == 0); // Some(&2)
let pos = v.iter().position(|&x| x == 3); // Some(2)
for_each
Apply a closure to each item:
v.iter().for_each(|x| println!("{}", x));
The conventional Rust style favours for loops over for_each for printing and side effects:
for x in &v {
println!("{}", x);
}
for_each is conventional in chains, where the for-loop equivalent is awkward.
last, nth
let last = (1..=10).last(); // Some(10)
let third = (1..=10).nth(2); // Some(3)
Common patterns
Map and collect
let nums = vec![1, 2, 3, 4, 5];
let strings: Vec<String> = nums.iter().map(|n| n.to_string()).collect();
Filter and collect
let evens: Vec<i32> = (1..=20).filter(|n| n % 2 == 0).collect();
Map-filter chain
let result: Vec<i32> = (1..=100)
.map(|n| n * n)
.filter(|&n| n % 3 == 0)
.take(10)
.collect();
Sum a transform
let total: i32 = v.iter().map(|x| x * x).sum();
Group and count
use std::collections::HashMap;
fn count_occurrences<T: Eq + std::hash::Hash>(items: impl IntoIterator<Item = T>) -> HashMap<T, i32> {
items.into_iter().fold(HashMap::new(), |mut map, item| {
*map.entry(item).or_insert(0) += 1;
map
})
}
Find with predicate
let v = vec!["alice", "bob", "charlie"];
let starts_with_b = v.iter().find(|s| s.starts_with('b')); // Some(&"bob")
Sort
The standard library’s sort is on the slice (not the iterator):
let mut v = vec![3, 1, 4, 1, 5];
v.sort(); // in place
println!("{:?}", v); // [1, 1, 3, 4, 5]
let mut v = vec!["banana", "apple", "cherry"];
v.sort_by_key(|s| s.len());
To sort while iterating, collect to a Vec and sort:
let mut sorted: Vec<i32> = (1..=10).collect();
sorted.sort();
Iterating in parallel (rayon)
The third-party rayon crate admits parallel iteration:
use rayon::prelude::*;
let v: Vec<i32> = (1..=1_000_000).collect();
let sum: i64 = v.par_iter().map(|&x| x as i64).sum();
The par_iter() produces a parallel iterator; the API mirrors Iterator.
Zip-and-sum
let xs = vec![1.0, 2.0, 3.0];
let ys = vec![4.0, 5.0, 6.0];
let dot: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| x * y).sum(); // 32.0
Enumerate-and-pair
let words = vec!["zero", "one", "two", "three"];
let indexed: HashMap<usize, &&str> = words.iter().enumerate().collect();
A note on laziness
Adapters do no work until a consumer triggers iteration:
let v = vec![1, 2, 3];
let mapped = v.iter().map(|x| {
println!("processing {}", x);
x * 2
});
// nothing printed yet
let collected: Vec<i32> = mapped.collect();
// now "processing 1", "processing 2", "processing 3" prints
The mechanism admits substantial efficiency: chain operations build up a fused operation; the consumer drives it once. The compiler often inlines and optimises the entire chain, producing machine code equivalent to a hand-written loop.
A note on Iterator vs IntoIterator
Many APIs take IntoIterator rather than Iterator:
fn process<I>(iter: I) where I: IntoIterator<Item = i32> {
for x in iter {
// ...
}
}
// Now any of these work:
process(vec![1, 2, 3]);
process(1..=10);
process([1, 2, 3]);
IntoIterator admits “anything that can be turned into an iterator”; Iterator is the iterator itself. The conventional API takes IntoIterator for substantial flexibility.
A note on the ? operator
The ? operator works in iterator chains via collect():
fn parse_all(strs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
strs.iter().map(|s| s.parse::<i32>()).collect()
}
The Result<Vec<T>, E> from-iterator instance: collects all Ok values; short-circuits on the first Err.
A note on the conventional discipline
The contemporary Rust iterator advice:
- Prefer iterator chains over loops for transformation pipelines.
- Use
forloops for side effects;for_eachonly in chains. - Use
collect::<Vec<_>>()(turbofish) when the target type isn’t obvious. - Use
filter_mapfor combined filter-and-map. - Use
flat_mapfor “map and flatten”. - Use
enumeratefor index-and-value iteration. - Use
zipfor parallel iteration. - Trust the compiler — the compiled chain is as fast as a hand-written loop.
The combination — lazy adapters, eager consumers, the substantial standard library, generic over IntoIterator — is one of Rust’s most distinctive features. The mechanism admits substantial functional-style code with no runtime overhead.