Lazy Evaluation
Deferring computation until the result is actually needed.
Overview
Lazy evaluation defers the computation of a value until it is actually needed. This enables infinite data structures, avoids unnecessary work, and can dramatically improve performance when only a fraction of a large collection is consumed.
Origin
Lazy evaluation was formalised in the context of ALGOL 60 and is the default evaluation strategy in Haskell. Languages like Scala, Python, and Ruby support it through generators and lazy enumerators.
Examples
Lazy enumerator in Ruby
# Without lazy: builds the entire mapped array before taking 3
(1..Float::INFINITY).map { |n| n * n }.first(3) # runs forever
# With lazy: stops as soon as 3 values are yielded
result = (1..Float::INFINITY).lazy.map { |n| n * n }.first(3)
# => [1, 4, 9]
# Chained lazy pipeline, no intermediate arrays created
expensive_pipeline = (1..Float::INFINITY)
.lazy
.select { |n| n.odd? }
.map { |n| n ** 3 }
.reject { |n| n % 7 == 0 }
.first(5)Each element flows through the full pipeline one at a time. No intermediate arrays exist. Memory usage is O(1) regardless of input size.
JavaScript generator as lazy sequence
function* naturals(start = 1) {
while (true) yield start++
}
function* map(iter, fn) {
for (const value of iter) yield fn(value)
}
function* filter(iter, pred) {
for (const value of iter) if (pred(value)) yield value
}
function take(iter, n) {
const result = []
for (const value of iter) {
result.push(value)
if (result.length === n) break
}
return result
}
const squares = map(naturals(), n => n * n)
const oddSquares = filter(squares, n => n % 2 !== 0)
take(oddSquares, 5) // [1, 9, 25, 49, 81]Use Cases
- 01Paginating through large datasets without loading everything into memory
- 02Processing log files or streams line-by-line
- 03Generating infinite sequences (IDs, dates, configurations) and consuming a bounded window
- 04Short-circuit evaluation: combining lazy streams with early termination
- 05Building efficient data pipelines where only a subset of transformed data is consumed
When Not to Use
- //When every element will be consumed, the lazy overhead is wasted
- //When the order of side effects matters: lazy sequences can make timing unpredictable
- //In multi-threaded contexts where lazy sequences are shared across threads without synchronisation
- //When the team needs to reason about when computation happens, eager evaluation is more predictable
Technical Notes
- Ruby's Enumerator::Lazy wraps any enumerable and applies transformations on pull. Converting back to eager with .to_a or .force triggers evaluation
- JavaScript generators are pull-based: values are produced only when the consumer calls .next()
- Haskell is entirely lazy by default, which enables elegant infinite structures but makes reasoning about memory and performance harder, "space leaks" are a common Haskell pitfall
- Database cursors are a form of lazy evaluation, rows are fetched as the application iterates, not all at once
More in Programming Techniques