R
RUSTY YELLOW PAGES

Lazy evaluationConcept


Explanation

Laziness in Rust's iterators means computation is deferred until it's demanded, rather than run eagerly as each step is written. 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 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

Scenario

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.

Scenario

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.

Explanation

Embedded support: Full

Laziness is a property of core::iter's adaptors, not of std, so it holds exactly as written on a #![no_std] target: building a chain with map/filter does no work and allocates nothing until something pulls values through it. This matters even more on a memory-constrained target than on a hosted one — because no intermediate buffer is ever materialized between adaptor stages, a long chain over a fixed-size array of readings still costs nothing beyond the final values actually produced, and the compiler typically fuses the whole chain into one loop. Laziness also means a chain can be built over a source that's slow to fully drain — a peripheral FIFO being drained one register read at a time — and still be handed to a caller that only wants the first few values, without ever pulling more reads than requested.

Basic usage example

let register_bank: [u16; 6] = [10, 45, 90, 200, 512, 1023];

let scaled = register_bank.iter().map(|&r| r / 2); // <- map builds a pipeline; nothing computed yet, no allocation

let first_two: [u16; 2] = {
    let mut it = scaled.take(2); // still lazy
    [it.next().unwrap(), it.next().unwrap()] // work happens here, one value at a time
};
assert_eq!(first_two, [5, 22]);

Best practices & deeper information

Scenario

Working with collections

Searching a buffer of recent sensor samples for the first one past a threshold only needs to look as far as the first match — laziness means the rest of the buffer is never touched, and nothing is ever collected into a Vec to search through.

let samples: [u16; 8] = [480, 495, 510, 605, 490, 500, 512, 498];

let first_over_threshold = samples
    .iter()
    .filter(|&&s| s > 600) // <- filter is lazy: nothing evaluated until pulled
    .next(); // <- stops at the first match, ignoring the rest of the buffer

assert_eq!(first_over_threshold, Some(&605));

Why this way: stopping at the first match without ever building an intermediate collection keeps the whole search on the stack with a bounded, small number of comparisons — exactly the property that matters when the buffer represents scarce peripheral reads rather than free in-memory data.

Scenario

Designing a public API

A driver function that reports which channel readings crossed a threshold shouldn't build a heapless::Vec of every match if the caller only wants the first — returning a lazy iterator over a borrowed fixed buffer lets the caller decide how much to consume, with no allocation forced either way.

struct ChannelLog {
    readings: [i16; 4],
}

impl ChannelLog {
    fn above_threshold(&self, threshold: i16) -> impl Iterator<Item = i16> + '_ {
        // <- returns a lazy iterator borrowing `readings`; caller decides how much to consume
        self.readings.iter().copied().filter(move |&r| r > threshold)
    }
}

let log = ChannelLog { readings: [18, 24, 30, 21] };
let first_spike = log.above_threshold(25).next();
assert_eq!(first_spike, Some(30));

Why this way: returning impl Iterator<Item = i16> instead of a collected buffer defers work until the caller actually asks for it, and because the iterator borrows the fixed array rather than owning a new allocation, the whole API stays viable with zero heap on a #![no_std] target.