Skip to content

Lazy Sequence Generator

Iteration tool — instantiates Demand-Triggered Deferred Evaluation

Produces a sequence one element at a time, computing each only when the consumer pulls for it — and never computing elements no one asks for.

A lazy sequence generator produces the elements of a sequence one at a time, computing each only when a consumer pulls for it, and computing nothing at all for elements no one asks for. Implemented as generators, iterators, or lazy streams, it can represent sequences too large to hold in memory or even conceptually infinite, because the existence of an element is deferred until the moment of the pull. What makes it THIS mechanism is suspend-and-resume: between one element and the next, the producer's execution is frozen mid-computation — local variables, loop position, buffers and all — and resumed exactly there on the next pull, so the sequence is generated incrementally rather than built and returned whole.

Example

A program must find the first 50 log lines matching a pattern in an 80-GB compressed log. A lazy line generator yields one decompressed line per pull; the consumer pulls, tests, keeps or discards, and stops once it has 50 matches. Between pulls the generator's state — file offset, decompressor buffers — stays frozen and resumes on the next next(), so the remaining ~99% of the file is never decompressed. An eager read would have inflated all 80 GB into memory before the first test.

The same shape handles the unbounded case. A naturals() generator yields 0, 1, 2, … with no end; take(50) over it terminates not because the generator stops but because the consumer stops pulling. The producer never needs to know the sequence has a length.

How it works

  • Pull, don't push. Each next() advances the producer by exactly one element, then re-suspends; nothing runs between pulls.
  • Freeze and resume execution. The generator's local frame — loop counter, cursor, buffers — persists across suspensions and resumes precisely where it yielded.
  • Let the consumer bound the length. For huge or infinite sources, the consumer (take, first, until) decides when to stop; the producer need not.
  • Read captured state lazily. Inputs the generator closed over are read as elements are produced, not up front.

Tuning parameters

  • Chunk/batch size — one element per pull vs. a buffered batch. Batching cuts per-pull overhead but over-produces past where the consumer stops.
  • Buffering / read-ahead — pure pull vs. prefetching the next elements. Prefetch hides latency but may compute elements never consumed.
  • Single-pass vs. re-iterable — consumed once vs. replayable. Re-iterable costs re-computation or storage; single-pass is cheapest but cannot be traversed twice.
  • Backpressure coupling — how tightly production rate follows consumption. Tight coupling bounds memory; loose coupling smooths bursts at the cost of buffered waste.

When it helps, and when it misleads

Its strength is huge or unbounded sequences, early termination, and pipelines where each stage's full output is rarely consumed — the generator turns "process a stream" into constant memory and pay-per-element cost.

It misleads through its frozen closure. A generator can capture mutable state that changes underneath it, so the sequence you pull differs from the one you expected — a lazy-capture bug that surfaces far from its cause. A single-pass generator silently yields nothing on a second traversal, and an open generator pins its resources (file handles, cursors) until it is exhausted or closed. The classic misuse is forcing the whole thing into a list "just to be safe," which discards every benefit and simply never terminates on an infinite source. The discipline is to bound infinite sources at the consumer and treat captured state as read-only for the generator's lifetime.[1]

How it implements the components

Lazy Sequence Generator fills the incremental-pull side of the archetype — one element per demand, with resumable state — not caching or gating:

  • demand_signal — each next() pull is the demand that yields exactly one more element.
  • suspended_evaluation_state — the frozen producer frame that resumes at the last yield point.
  • cycle_and_nontermination_guard — infinite or huge sequences require the consumer to bound the pull, or realization never terminates.
  • input_lifetime_and_capture_rule — closed-over inputs must stay valid and stable across suspensions, since they are read lazily.

It does not cache elements for reuse across passes — that is Memoization — nor wrap a single deferred value with force-once semantics, which is the Thunk or Lazy Promise.

References

[1] In pull-based (demand-driven) streams, an infinite sequence is well-defined precisely because a consumer's take/head bounds how much is realized — a standard property of lazy lists, as in Haskell. It is also why forcing such a sequence in full cannot terminate.