Lazy evaluationConcept
Explanation
Building an adaptor chain does no work by itself.
Writing data.iter().map(f).filter(g) only constructs a small nested
struct describing what would happen to an item if one arrived — no
call to f or g actually happens at that line. Work only starts when
something pulls values through the chain: a for loop, or a
consumer like sum or collect. This is what
"lazy" means here — computation is deferred until it's demanded, rather
than run eagerly as each step is written.
This deferral is what makes infinite iterators usable at all. A range
like 0.. never terminates on its own, but because nothing runs until
requested, it's perfectly safe to build adaptors on top of it as long as
something downstream — take, find, or a manual break — decides when
to stop asking for more. An eager language would have to materialize the
whole sequence (or a huge prefix of it) before any transformation could
even be applied.
Laziness is also why long chains stay cheap: because no intermediate collection is ever built between adaptors, the compiler can typically fuse the whole chain into a single loop that computes one final item at a time, directly from the source, with no allocation in between. This is the flip side of the same property described in Iterator adaptors — the wrapping approach and the laziness are two views of the same mechanism.
The most common gotcha follows directly from this: an adaptor chain built
for its side effects (say, a map closure that prints something) and
then never consumed will do nothing at all, silently. The chain isn't
broken — it was simply never asked to run. The upside of the same
property is architectural: a function can return impl Iterator<Item = T> instead of an eagerly built Vec<T>, letting the caller decide how
much of the sequence to actually pay for.
Basic usage example
let numbers = 1..; // an infinite range
let doubled = numbers.map(|n| n * 2); // <- map builds a pipeline here; nothing has been computed yet
let first_three: Vec<i32> = doubled.take(3).collect(); // work only happens here, when collect pulls values
assert_eq!(first_three, vec![2, 4, 6]);
Best practices & deeper information
Working with collections
Searching a huge stream of generated reading ids for the first one past a threshold only needs to compute as far as the first match — laziness means the rest is never touched.
let reading_ids = (1..=1_000_000).map(|n| n * 10); // <- a lazy pipeline over a million ids; nothing has been computed yet
let first_over_threshold = reading_ids
.filter(|&id| id > 500_000)
.next(); // <- only pulls values until the first match, stopping early
assert_eq!(first_over_threshold, Some(500_010));
Why this way: because map and filter are lazy, .next() stops
the moment it finds a match instead of materializing a million-element
Vec first — the
Iterator trait docs
describe adaptors as producing values "on demand" for exactly this
reason.
Designing a public API
A method that reports which sensor readings crossed a threshold doesn't
need to build a Vec of all of them if the caller only wants the first —
returning a lazy iterator lets the caller decide.
struct SensorLog {
readings: Vec<f64>,
}
impl SensorLog {
pub fn above_threshold(&self, threshold: f64) -> impl Iterator<Item = f64> + '_ {
// <- returns a lazy iterator; the caller decides how much to actually consume
self.readings.iter().copied().filter(move |&r| r > threshold)
}
}
let log = SensorLog { readings: vec![68.1, 72.4, 75.0, 69.9] };
let spike = log.above_threshold(70.0).next();
assert_eq!(spike, Some(72.4));
Why this way: returning impl Iterator<Item = f64> instead of
Vec<f64> defers the cost of computing (and allocating) every match
until — and unless — the caller actually asks for it, an API design
principle Effective Rust frames as
preferring the laziest return type that still satisfies every caller.