Why this matters
Loops that transform, filter and summarize data are the bread and butter of programming. Rust's iterators let you express them as pipelines: "take the lines, skip blank ones, parse each, keep the positive ones, sum them". The result is often shorter and clearer than the equivalent loop, harder to get wrong (no index bookkeeping), and just as fast: the compiler turns iterator chains into the same machine code as a hand-written loop. Iterators are also how for loops work underneath.
The Iterator trait
An iterator is any type implementing this trait (simplified):
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... plus about 75 provided methods built on next()
}
next returns Some(item) until the sequence is exhausted, then None. type Item is an associated type naming what it yields. A for loop just calls next until None:
fn main() {
let v = vec![10, 20, 30];
let mut it = v.iter();
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
}
Some(10)
Some(20)
Some(30)
None
next takes &mut self because it advances the iterator's internal position.
Three ways to iterate a collection
| method | yields | collection afterwards |
|---|---|---|
v.iter() | &T | still usable |
v.iter_mut() | &mut T | still usable, possibly modified |
v.into_iter() | T (owned) | consumed |
for x in &v, for x in &mut v and for x in v call these three. Ranges, chars(), split_whitespace(), lines(), map keys and values are all iterators too.
Adapters: transforming lazily
Adapters take an iterator and return a new one. They do nothing until something consumes the result:
fn main() {
let v = vec![1, 2, 3, 4, 5, 6];
let lazy = v.iter().map(|x| x * 10);
println!("nothing computed yet");
let result: Vec<i32> = lazy.filter(|x| x % 20 == 0).collect();
println!("{:?}", result);
v.iter().map(|x| x * 2);
}
nothing computed yet
[20, 40, 60]
The compiler warns about the last line: an adapter whose result is never used does nothing at all (unused_must_use: "iterators are lazy and do nothing unless consumed").
The common adapters:
fn main() {
let words = ["apple", "bob", "kayak", "rust", "level"];
let nums = [3, 1, 4, 1, 5, 9, 2, 6];
let palins: Vec<_> = words.iter().filter(|w| w.chars().rev().eq(w.chars())).collect();
let lens: Vec<_> = words.iter().map(|w| w.len()).collect();
let indexed: Vec<_> = words.iter().enumerate().skip(1).take(2).collect();
let pairs: Vec<_> = words.iter().zip(nums.iter()).collect();
let both: Vec<_> = nums.iter().take(2).chain(nums.iter().rev().take(2)).collect();
let letters: Vec<char> = words.iter().take(2).flat_map(|w| w.chars()).collect();
let until_big: Vec<_> = nums.iter().take_while(|&&n| n < 5).collect();
let windows: Vec<i32> = nums.windows(2).map(|w| w[1] - w[0]).collect();
println!("{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}", palins, lens, indexed, pairs, both, letters, until_big, windows);
}
["bob", "kayak", "level"]
[5, 3, 5, 4, 5]
[(1, "bob"), (2, "kayak")]
[("apple", 3), ("bob", 1), ("kayak", 4), ("rust", 1), ("level", 5)]
[3, 1, 6, 2]
['a', 'p', 'p', 'l', 'e', 'b', 'o', 'b']
[3, 1, 4, 1]
[-2, 3, -3, 4, 4, -7, 4]
map(f)transforms each item;filter(p)keeps items wherepis true;filter_map(f)does both with anOption.enumerate()pairs items with indexes;zip(other)pairs two iterators, stopping at the shorter.take(n),skip(n),take_while(p),skip_while(p)limit the sequence;rev()reverses (when possible).chain(other)concatenates;flat_map(f)maps each item to an iterator and flattens the results.
A note on the &&x patterns: nums.iter() yields &i32, and filter passes a reference to each item, so its closure receives &&i32. Patterns like |&&n| peel both layers; |n| **n < 5 does the same.
Consumers: producing a result
Consumers pull items through the chain and produce a final value:
fn main() {
let nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
let total: i32 = nums.iter().sum();
let product: i64 = nums.iter().map(|&x| x as i64).product();
let count = nums.iter().filter(|&&x| x > 2).count();
let max = nums.iter().max();
let min_pos = nums.iter().enumerate().min_by_key(|(_, &v)| v).map(|(i, _)| i);
let any_big = nums.iter().any(|&x| x > 8);
let all_pos = nums.iter().all(|&x| x > 0);
let first_even = nums.iter().find(|&&x| x % 2 == 0);
let pos = nums.iter().position(|&x| x == 5);
let joined = nums.iter().map(|n| n.to_string()).collect::<Vec<_>>().join("-");
let folded = nums.iter().fold(0, |acc, &x| acc * 10 + x);
println!("{total} {product} {count} {:?} {:?} {any_big} {all_pos} {:?} {:?}", max, min_pos, first_even, pos);
println!("{joined} {folded}");
}
31 6480 5 Some(9) Some(1) true true Some(4) Some(4)
3-1-4-1-5-9-2-6 31415926
collect()builds a collection:Vec,String,HashMap(from pairs),HashSet, evenResult<Vec<_>, E>(stopping at the first error). The target type comes from an annotation or turbofish.sum,product,count,min,max,min_by_key,max_by_key.any,all,find,positionstop early as soon as the answer is known.fold(init, f)is the general accumulator: everything else can be written with it.
How laziness works, and why it is fast
Each adapter is a small struct wrapping the previous iterator. Calling next on the outermost one calls next on the one inside, and so on: items flow through the whole chain one at a time:
sum() --next--> Filter --next--> Map --next--> slice::Iter --> [1, 2, 3, 4, 5, 6]
<-Some(40)- <-Some(40)- <-Some(&4)-
No intermediate Vec is created between stages, and stages stop early when the consumer is done. Because every adapter is generic and the closures are known at compile time, the optimizer inlines the whole chain into a single loop. This is the "zero-cost abstraction" promise from module 10 in action.
collect into Result: parsing everything or failing
fn main() {
let good = "1 2 3";
let bad = "1 two 3";
let a: Result<Vec<i32>, _> = good.split(' ').map(|s| s.parse::<i32>()).collect();
let b: Result<Vec<i32>, _> = bad.split(' ').map(|s| s.parse::<i32>()).collect();
println!("{:?}\n{:?}", a, b);
}
Ok([1, 2, 3])
Err(ParseIntError { kind: InvalidDigit })
Collecting an iterator of Results into Result<Vec<_>, _> gives Ok(vec) if all succeeded, or the first Err. Combined with ? it makes "parse every line or report the first problem" a one-liner.
Common mistakes
- Forgetting a consumer: the chain does nothing; the compiler warns
unused_must_use. collect()without a target type: E0283 "type annotations needed". Annotate the variable or usecollect::<Vec<_>>().- Reference layers in closures:
filter(|x| x > 2)oniter()fails with E0308 (comparing&&i32with an integer). Use|&&x|or|x| **x > 2. - Using
into_iter()and then the collection: E0382; useiter()if you still need it. - Overlong chains: readability matters. A plain
forloop is fine when a chain gets clever.
Recap
- An iterator implements
next() -> Option<Item>;forloops call it untilNone. iter(),iter_mut(),into_iter()yield&T,&mut T,T.- Adapters (
map,filter,enumerate,zip,take,skip,chain,flat_map,rev) are lazy. - Consumers (
collect,sum,count,fold,any,all,find,min/max) drive the chain. - Chains process one item at a time with no intermediate allocations and compile to tight loops.
Going further: read the method list of Iterator in the standard library docs and try scan, step_by, peekable, partition and group-style counting with fold into a BTreeMap.