How it works
The explanation, with examples you can run.
Laziness, closures and consumers
fn main() { let words = ["apple", "kiwi", "banana", "fig"]; let lazy = words.iter().map(|w| { println!("looking at {w}"); w.len() }); // nothing printed yet: adapters do no work on their own println!("built the iterator"); let long: Vec<usize> = lazy.filter(|&n| n > 3).collect(); // now it runs println!("{long:?}"); let total: usize = words.iter().map(|w| w.len()).sum(); let mut sum_loop = 0; for w in &words { sum_loop += w.len(); } assert_eq!(total, sum_loop); // same result; in release builds, typically the same machine code too let mut counter = 0; let mut bump = || counter += 1; // FnMut closure: borrows counter mutably bump(); bump(); println!("{counter} {:?}", (1..=4).rev().step_by(2).collect::<Vec<_>>());}It prints built the iterator before any looking at lines, then the four looking at lines, [5, 4, 6], and 2 [4, 2]. Items flow through the chain one at a time: collect pulls one, which makes filter pull from map, which pulls from the array.
Three ways to iterate a collection
v.iter()yields&T: the collection is only borrowed.for x in &vis the same.v.iter_mut()yields&mut T, to change items in place.for x in &mut vis the same.v.into_iter()yieldsTand consumes the collection.for x in vis the same, sovis gone afterwards.
collect can build any type that implements FromIterator: a Vec, HashMap, String, and even Result<Vec<T>, E>, which stops at the first error.
Closures
A closure (|args| body) captures what it uses from its surroundings in the least demanding way that works: by shared reference, by mutable reference, or by value. That decides which of three traits it implements: Fn (only reads), FnMut (mutates captured state, like bump), or FnOnce (moves something out, so it can be called only once). move |...| forces captures by value, which threads and async tasks need. Adapters take closures as generic parameters, so they are inlined too.
Why it costs nothing
words.iter().map(f).filter(g) has a concrete type, Filter<Map<Iter<&str>, F>, G>, known at compile time. With optimisations on, the compiler inlines every next call and closure into one loop. Iterating a slice can also remove bounds checks that indexing v[i] would need. This is the meaning of a zero-cost abstraction: what you don't use you don't pay for, and what you do use you couldn't write better by hand. It only holds with optimisations, so measure with cargo build --release.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.