Skip to content
Home
Lazy Evaluation: Deferred Computation in FP

Lazy Evaluation: Deferred Computation in FP

Functional Programming Functional Programming 8 min read 1665 words Beginner ExcellentWiki Editorial Team

Lazy evaluation is a strategy where expressions are not evaluated until their results are actually needed. Also called call-by-need, lazy evaluation enables powerful programming patterns that are impossible or impractical with eager evaluation. Haskell is the most famous lazy language, but lazy constructs appear in many languages.

Eager vs Lazy Evaluation

In eager evaluation (call-by-value), arguments are evaluated before the function is called:

def eager_square(n):
    return n * n

# n is evaluated before square runs
result = eager_square(3 + 4)  # 7 evaluated first, then 49

In lazy evaluation, arguments are passed unevaluated and only computed when needed:

-- Haskell is lazy by default
let result = square (3 + 4)  -- 3+4 is NOT evaluated yet
-- Only when result is printed or used does the computation happen
print result  -- Now 3+4 is computed, then squared

Benefits of Lazy Evaluation

Infinite Data Structures

Lazy evaluation makes infinite data structures practical:

-- An infinite list of natural numbers
naturals = [1..]

-- An infinite list of Fibonacci numbers
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

-- Only the first 10 are ever computed
take 10 fibs  -- [0,1,1,2,3,5,8,13,21,34]

In an eager language, constructing an infinite list would cause infinite recursion. In Haskell, only the elements actually needed are computed.

Separation of Generation from Consumption

Lazy evaluation lets you separate what data is generated from how much is consumed:

-- Generate all primes (conceptually infinite)
primes = sieve [2..]
  where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]

-- Consume only what you need
firstHundredPrimes = take 100 primes

The sieve generates primes, but the generation pauses between take calls. This compositional style lets you write general, reusable generators.

Improved Modularity

Lazy evaluation enables glue code that would otherwise require manual orchestration:

-- Reading a file and processing lines
processFile = do
    contents <- readFile "data.txt"
    let lines' = lines contents
        processed = processLines lines'
    writeFile "output.txt" (unlines processed)

Even though readFile reads the entire file conceptually, lazy I/O actually reads data on demand. Lines are read, processed, and written incrementally. This keeps memory usage constant regardless of file size.

Avoiding Unnecessary Computation

Lazy evaluation naturally avoids computing unused branches:

-- Only one branch is evaluated
if condition
    then expensiveComputation1
    else expensiveComputation2

This is like short-circuit evaluation (&&, ||) extended to all expressions.

Implementing Lazy Evaluation

Lazy Sequences in Python

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]

Python generators are lazy — they produce values on demand and can represent infinite sequences.

Lazy Properties in C#

IEnumerable<int> GetNumbers() {
    for (int i = 0; ; i++) {
        yield return i;
    }
---

var numbers = GetNumbers();
var first10 = numbers.Take(10).ToList();

LINQ queries in C# are lazily evaluated. The query is not executed until you iterate over it.

Lazy Initialization in Java

public class ExpensiveObject {
    private static class Holder {
        static final ExpensiveObject INSTANCE = new ExpensiveObject();
    }

    public static ExpensiveObject getInstance() {
        return Holder.INSTANCE;  // Created on first access
    }
---

The Initialization-on-demand holder idiom uses the JVM’s class loading mechanism for thread-safe lazy initialization.

Lazy Thunks in JavaScript

// Simulating lazy evaluation with thunks
function lazy(fn) {
    let evaluated = false;
    let result = null;
    return function() {
        if (!evaluated) {
            result = fn();
            evaluated = true;
        }
        return result;
    };
---

const expensive = lazy(() => {
    console.log("Computing...");
    return 42;
---);

console.log(expensive()); // "Computing..." then 42
console.log(expensive()); // 42 (cached, no recomputation)

Memoization

Lazy evaluation naturally supports memoization — once a lazy value is computed, the result is cached:

-- In Haskell, once 'result' is evaluated, subsequent accesses
-- use the cached value
result = expensiveComputation 100

Manual memoization in eager languages:

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

Drawbacks and Trade-offs

Space Leaks

Lazy evaluation can cause memory leaks when unevaluated thunks accumulate:

-- This may use O(n) memory because of accumulated thunks
foldl (+) 0 [1..1000000]

-- This uses O(1) memory (strict evaluation)
foldl' (+) 0 [1..1000000]

The strict version foldl' forces evaluation at each step, preventing thunk buildup.

Performance Predictability

Lazy evaluation makes runtime performance harder to predict. An expression may be evaluated zero times, once, or (rarely) multiple times. Understanding when thunks are forced requires familiarity with the evaluation model.

Debugging Difficulty

Execution order in lazy languages is determined by demand, not by program text. This can make debugging confusing because stack traces may not reflect the logical program flow.

When to Use Lazy Evaluation

Use lazy evaluation for infinite data structures, streaming large datasets, avoiding unnecessary computation, and improving modularity through separation of generation and consumption. Use eager evaluation when performance predictability matters, when memory is constrained, or when side effects must occur in a specific order.

FAQ

What is a thunk?

A thunk is a suspended computation — a value that hasn’t been evaluated yet. In lazy languages, every unevaluated expression is stored as a thunk. When its value is needed, the thunk is forced (evaluated) and the result is cached for subsequent accesses.

Does JavaScript support lazy evaluation?

JavaScript is eagerly evaluated by default. However, generators (function*) and iterators provide lazy sequences. Libraries like Lodash offer lazy evaluation for method chains.

How does lazy evaluation affect exception handling?

Exceptions in lazy code may be deferred until the value is forced. This means an exception might be thrown far from where the error originated, making debugging harder. In Haskell, this is managed through the IO monad which forces sequential, deterministic evaluation.

What is the difference between lazy evaluation and short-circuit evaluation?

Short-circuit evaluation (&&, ||) is a limited form of laziness for boolean expressions. Lazy evaluation extends this principle to all expressions. Short-circuit stops evaluating when the result is determined; lazy evaluation defers all computation until needed.

Can I combine lazy and eager evaluation in the same codebase?

Yes. Most languages that support lazy evaluation also provide strict evaluation when needed. In Haskell, seq and deepseq force evaluation. In Python, list() forces a generator. Choose the evaluation strategy based on your specific needs for performance, memory, and predictability.


Related: Explore recursion patterns and pure functions.

Lazy Evaluation Deep Dive

Lazy evaluation postpones expression evaluation until the result is needed. Haskell’s non-strict evaluation is the purest form: nothing evaluates unless required by pattern matching or seq. Benefits: infinite data structures (fibs = 0:1:zipWith (+) fibs (tail fibs)), avoiding unnecessary computation, and improved modularity. Thunks (suspended computations) are the mechanism. Forcing a thunk evaluates it and memoizes the result. Lazy initialization in impure languages: lazy val in Scala, Lazy<T> in C#, Python generators. JavaScript iterators and generators provide lazy sequences. Lazy evaluation can cause space leaks if thunks accumulate unevaluated.

Trade-offs

Lazy evaluation improves performance when computing expensive values that may not be needed or processing large datasets via streaming. It degrades when every result is needed (adds thunk overhead) or when low latency is critical. Hybrid approaches use lazy collections with strict terminals. In Python, prefer generator expressions over list comprehensions for large data.

Lazy Evaluation in Practice: Streaming and Big Data

Lazy evaluation is foundational to modern data processing pipelines. Apache Spark uses lazy evaluation throughout: transformations like map, filter, and flatMap build a directed acyclic graph (DAG) of operations, and only actions like count, collect, or save trigger actual computation. This allows Spark’s Catalyst optimizer to rearrange and fuse operations for maximum performance. In Java, the Stream API (stream().map().filter().collect()) is lazily evaluated — intermediate operations produce no results until a terminal operation is invoked. MongoDB cursors return results lazily, fetching batches only as the application iterates. In frontend development, virtual scrolling libraries render only visible rows by lazily computing layout positions. RxJS Observable pipelines compose asynchronous data streams lazily — nothing executes until a subscription calls subscribe(). Understanding lazy evaluation helps developers design efficient, memory-conscious data pipelines that scale to arbitrarily large datasets. In functional languages, combining lazy evaluation with immutable data structures enables purely functional streaming — processing datasets larger than available RAM without explicit memory management. This pattern underpins Haskell’s conduit and pipes libraries, Scala’s FS2, and Java’s reactive streams. For backend services, lazy loading of database results via pagination or cursor-based iteration prevents out-of-memory errors when fetching large result sets. ORM libraries like Hibernate, Entity Framework, and SQLAlchemy all support lazy loading of related entities by default, deferring database queries until the property is accessed.

Related Concepts and Further Reading

Understanding lazy evaluation requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.

The relationship between lazy evaluation and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.

For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of lazy evaluation. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.

Section: Functional Programming 1665 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top