Iterators in Rust
Understand the Iterator trait, lazy evaluation, adapter chaining, and how to write custom iterators.
The Iterator Trait
Iterators are the idiomatic way to process sequences of values in Rust. Rather than writing index-based for loops, you compose adapters that describe what transformations to apply. This style is not just cleaner — the compiler can often optimize a chain of adapters into a single tight loop with no intermediate allocations. Everything in Rust’s iteration system is built on one simple trait:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 70+ methods with default implementations built on top of next()
}
When next() returns None, iteration is complete. All other iterator methods — map, filter, zip, sum, and so on — have default implementations that call next(), so you get them for free by implementing just one method.
The Three Iteration Methods
Collections expose three different ways to iterate, giving you control over ownership. The right choice depends on whether you need to keep using the collection afterwards, whether you need to mutate elements, or whether you want to consume and transform the values.
fn main() {
let v = vec![1, 2, 3];
// iter() — borrows the collection, yields &T, collection remains usable
for x in v.iter() {
print!("{} ", x); // x is &i32
}
println!();
// iter_mut() — mutably borrows, yields &mut T, allows in-place modification
let mut v2 = vec![1, 2, 3];
for x in v2.iter_mut() {
*x *= 2; // double each element in place
}
println!("{:?}", v2); // [2, 4, 6]
// into_iter() — consumes the collection, yields owned T values
let v3 = vec![1, 2, 3];
for x in v3.into_iter() {
print!("{} ", x); // x is i32 (owned)
}
println!();
// v3 is moved; cannot be used after this point
}
In a for loop, for x in collection calls into_iter() implicitly.
Lazy Evaluation
Iterator adapters are lazy — they describe what to do but do no actual work until a consuming method drives them. This means you can build up a long pipeline without allocating intermediate collections, and you can even describe infinite sequences and only compute as many elements as you need.
fn main() {
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Building the pipeline does no computation — it just creates iterator structs
let pipeline = v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x);
// collect() is the consuming adapter that drives the pipeline and allocates the result
let result: Vec<i32> = pipeline.collect();
println!("{:?}", result); // [4, 16, 36, 64, 100]
}
Laziness enables infinite sequences — take stops the pipeline after enough elements are produced:
fn main() {
// Fibonacci as an infinite iterator — no allocation until take() bounds it
let fibs = std::iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b)))
.map(|(a, _)| a);
let first_ten: Vec<u64> = fibs.take(10).collect();
println!("{:?}", first_ten); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
Common Adapters
Transforming: map, flat_map, flatten
map transforms each element independently. flat_map transforms each element into an iterator and then concatenates all those iterators — useful when each input produces multiple outputs. flatten does just the concatenation step if you already have an iterator of iterators.
fn main() {
let words = vec!["hello world", "foo bar"];
// map — one-to-one transformation
let upper: Vec<String> = words.iter().map(|s| s.to_uppercase()).collect();
println!("{:?}", upper);
// flat_map — one-to-many: each string produces multiple words
let individual: Vec<&str> = words.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", individual); // ["hello", "world", "foo", "bar"]
// flatten — collapses one level of nesting
let nested = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
println!("{:?}", flat); // [1, 2, 3, 4]
}
Filtering: filter, filter_map, take_while, skip_while
filter keeps elements matching a predicate. filter_map combines filtering and transformation in one step — elements that map to None are discarded, elements that map to Some(v) produce v. This is the idiomatic way to parse or convert a sequence while skipping invalid entries.
fn main() {
let numbers = vec!["1", "two", "3", "four", "5"];
// filter_map — parse each string, discard parse failures (None), keep successes
let parsed: Vec<i32> = numbers.iter()
.filter_map(|s| s.parse().ok())
.collect();
println!("{:?}", parsed); // [1, 3, 5]
let data = vec![2, 4, 6, 1, 8, 10];
// take_while — yields elements until the predicate first fails, then stops
let prefix: Vec<i32> = data.iter().copied().take_while(|&x| x % 2 == 0).collect();
println!("{:?}", prefix); // [2, 4, 6]
// skip_while — skips elements until the predicate first fails, then yields the rest
let rest: Vec<i32> = data.iter().copied().skip_while(|&x| x % 2 == 0).collect();
println!("{:?}", rest); // [1, 8, 10]
}
Slicing: take, skip, step_by
These adapters let you work with a portion of a sequence without materializing the whole thing first. skip + take is the natural way to implement pagination over any iterator.
fn main() {
let v: Vec<i32> = (1..=20).collect();
// Page 2 of size 5 — skip the first 5, take the next 5
let page2: Vec<i32> = v.iter().copied().skip(5).take(5).collect();
println!("{:?}", page2); // [6, 7, 8, 9, 10]
// Every third element
let every_third: Vec<i32> = v.iter().copied().step_by(3).collect();
println!("{:?}", every_third); // [1, 4, 7, 10, 13, 16, 19]
}
Combining: zip, chain, enumerate
zip pairs up elements from two iterators — when either runs out, iteration stops. chain concatenates two iterators into one. enumerate adds a running index to any iterator, avoiding manual counter variables.
fn main() {
let names = vec!["Alice", "Bob", "Charlie"];
let scores = vec![95, 87, 92];
// zip — pair corresponding elements from two iterators
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
println!("{:?}", pairs);
// enumerate — (index, value) without a manual counter
for (i, name) in names.iter().enumerate() {
println!("{}: {}", i + 1, name);
}
// chain — treat two iterators as one sequential stream
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
println!("{:?}", combined); // [1, 2, 3, 4, 5, 6]
}
Consuming Adapters
Consuming adapters drive the iterator to completion and return a single result. They are the “trigger” that actually runs the pipeline. Common ones include collect, sum, fold, any, all, and find.
fn main() {
let v = vec![1, 2, 3, 4, 5];
println!("{}", v.iter().sum::<i32>()); // 15
println!("{}", v.iter().product::<i32>()); // 120
println!("{}", v.iter().count()); // 5
println!("{:?}", v.iter().min()); // Some(1)
println!("{:?}", v.iter().max()); // Some(5)
println!("{}", v.iter().any(|&x| x > 3)); // true — stops at first match
println!("{}", v.iter().all(|&x| x > 0)); // true — stops at first failure
println!("{:?}", v.iter().find(|&&x| x > 3)); // Some(4)
println!("{:?}", v.iter().position(|&x| x > 3)); // Some(3) — index of first match
// fold — general reduction with an accumulator; the basis for sum, product, etc.
let factorial = (1..=5u64).fold(1, |acc, x| acc * x);
println!("{}", factorial); // 120
// for_each — like a for loop but chainable in a pipeline
v.iter().for_each(|&x| print!("{} ", x));
println!();
}
Custom Iterators
Implementing Iterator on your own type makes it compatible with every adapter in the standard library. You define type Item and next() — the rest comes for free. This is one of the most powerful demonstrations of Rust’s trait system: a small interface contract unlocks an enormous amount of behavior.
// Iterates over every (x, y) coordinate in a 2D grid, row by row
struct Range2D {
width: usize,
height: usize,
cx: usize,
cy: usize,
}
impl Range2D {
fn new(width: usize, height: usize) -> Self {
Self { width, height, cx: 0, cy: 0 }
}
}
impl Iterator for Range2D {
type Item = (usize, usize);
// Define next() — all 70+ other Iterator methods are automatically available
fn next(&mut self) -> Option<(usize, usize)> {
if self.cy >= self.height { return None; }
let item = (self.cx, self.cy);
self.cx += 1;
if self.cx >= self.width {
self.cx = 0;
self.cy += 1;
}
Some(item)
}
}
fn main() {
for (x, y) in Range2D::new(3, 2) {
println!("({}, {})", x, y);
}
// (0,0) (1,0) (2,0) (0,1) (1,1) (2,1)
// filter, count, and every other adapter work automatically
let diagonal_count = Range2D::new(4, 4).filter(|(x, y)| x == y).count();
println!("diagonal cells: {}", diagonal_count); // 4
}
std::iter Constructors
The std::iter module provides functions for building common iterator patterns from scratch, without needing an existing collection to iterate over.
fn main() {
// repeat — infinite repetition of one value; always pair with take()
let threes: Vec<i32> = std::iter::repeat(3).take(5).collect();
println!("{:?}", threes); // [3, 3, 3, 3, 3]
// once — a single-element iterator; useful for prepending to a chain
let one: Vec<&str> = std::iter::once("hello").collect();
println!("{:?}", one); // ["hello"]
// empty — zero elements; useful as a neutral element in chains
let none: Vec<i32> = std::iter::empty().collect();
println!("{:?}", none); // []
// from_fn — generate elements with a closure that returns Option<T>
let mut n = 0u64;
let powers: Vec<u64> = std::iter::from_fn(move || {
n += 1;
Some(n * n)
}).take(5).collect();
println!("{:?}", powers); // [1, 4, 9, 16, 25]
}