Nugget 10
Home
You call .map() and nothing happens. Then .filter()
and still nothing. Finally .collect() and it all runs. This is
the single most important thing to understand about Rust iterators.
let v = vec![1, 2, 3, 4, 5];
v.iter().map(|x| {
println!("mapping {x}");
x * 2
});
// Nothing prints! No error, no output.
In JavaScript, arr.map(fn) runs immediately and returns a new array.
In Rust, map() just describes what should happen — it
doesn't actually do the work. The iterator is lazy.
map() returns a new iterator that remembers the transformation.
The actual work only happens when something calls next():
let v = vec![1, 2, 3]; let mapped = v.iter().map(|x| x * 10); // Nothing computed yet let mut iter = mapped; assert_eq!(iter.next(), Some(10)); // Now 1*10 happens assert_eq!(iter.next(), Some(20)); // Now 2*10 happens assert_eq!(iter.next(), Some(30)); // Now 3*10 happens assert_eq!(iter.next(), None); // Done
Each next() pulls one item through the entire pipeline. One item
at a time, no intermediate storage.
You can chain multiple transformations — they compose into a single lazy iterator that only runs when consumed:
let result: Vec<i32> = (1..=10)
.map(|x| x * 2) // Lazy: "remember to double each item"
.filter(|x| x > 10) // Lazy: "remember to keep only big ones"
.collect(); // Eager: NOW do all the work
println!("{:?}", result); // [12, 14, 16, 18, 20]
// collect() pulls items through:
// 1→2→no, 2→4→no, 3→6→no, 4→8→no, 5→10→no,
// 6→12→yes→store, 7→14→yes→store, ...
Each item flows through the entire pipeline before the next one starts. No
intermediate Vec is ever created between map and
filter.
// JavaScript — eager, creates intermediate arrays
[1, 2, 3, 4, 5]
.map(x => x * 2) // Creates [2,4,6,8,10] — full allocation
.filter(x => x > 5); // Creates [6,8,10] — second allocation
// Two arrays allocated, one thrown away.
// Rust — lazy, no intermediate allocation
(1..=5)
.map(|x| x * 2) // Just remembers the function
.filter(|x| x > 5) // Just remembers another function
.collect(); // One pass, one allocation
This is a fundamental design difference. Rust's approach means you can chain freely without worrying about extra allocations. It also means you can work with infinite sequences — more on that next.
Because iterators are lazy, you can create infinite sequences and only take what you need:
// Infinite range, but we only take 5
let first_five: Vec<i32> = (0..)
.skip(10)
.take(5)
.collect();
println!("{:?}", first_five); // [10, 11, 12, 13, 14]
// This would OOM in JS (or hang):
// [...Array(Infinity).keys()].map(x => x).filter(...).
// Rust is fine — nothing runs until collect() with .take() limiting it.
💡 0.. is an infinite range
(0..) starts at 0 and goes forever. It's only useful when
paired with take(), skip(), or another adapter
that limits the output.
map, filter) are lazy — they describe work but don't do it.collect() is eager — it pulls items through the pipeline and allocates the result.collect().0.. with take(n).