Dec Code Story

Chapter #7 – Lazy Evaluation

call-by-need, thunks, infinite streams, lazy vs strict

7.0  Prologue

Lazy evaluation delays computing a value until it is actually needed. This is not merely an optimization: it changes what programs can express. Infinite data structures, short-circuit evaluation as a library function, and demand-driven I/O all become natural when evaluation is driven by demand rather than by program order.

7.1  Call-by-Need Semantics

Under call-by-need, an argument to a function is evaluated at most once: the first time its value is demanded, and then the result is memoized (shared). This contrasts with:
  • Call-by-value (Rust, C, Python): arguments evaluated before the call.
  • Call-by-name: argument expression re-evaluated each time it is used (no sharing).
-- Haskell is call-by-need by default -- This is safe: the second argument is never evaluated safeDiv :: Int -> Int -> Maybe Int safeDiv _ 0 = Nothing safeDiv x y = Just (x `div` y) -- Short-circuit AND as a regular function (call-by-need makes this possible) myAnd :: Bool -> Bool -> Bool myAnd False _ = False -- second argument never demanded when first is False myAnd _ y = y In a strict language, myAnd False (error "boom") would crash. In Haskell it returns False because the second argument is never evaluated.

7.2  Thunks

A thunk is a suspended computation: a closure that captures the expression and evaluates it when forced. Haskell represents every unevaluated expression as a heap-allocated thunk. When a thunk is forced, the result overwrites the thunk (sharing). -- Haskell: explicit forcing with seq and $! -- Without forcing: thunks accumulate (space leak) sumLazy :: [Int] -> Int sumLazy = foldl (+) 0 -- builds a chain of thunks for large lists -- With forcing: strict accumulator via foldl' import Data.List (foldl') sumStrict :: [Int] -> Int sumStrict = foldl' (+) 0 -- evaluates accumulator at each step -- Rust: explicit laziness via closures (thunk idiom) let thunk = || expensive_computation(); // not yet called let value = thunk(); // forced here -- Python: generator expressions are lazy gen = (x * x for x in range(10**9)) -- no computation yet next(gen) -- forces first element only Thunks are the mechanism behind Haskell’s laziness. The primary hazard is space leaks: a long chain of unevaluated thunks can consume large amounts of heap memory before any value is demanded. foldl', seq, and BangPatterns are the standard tools for forcing evaluation where needed.

7.3  Infinite Streams and Sequences

Lazy evaluation makes infinite data structures safe. Only the portion that is demanded is ever materialized in memory. This enables elegant expression of sequences defined by recurrence relations, sieves, and other open-ended processes. -- Haskell: classic infinite sequences ones = 1 : ones -- [1,1,1,...] nats = [0..] -- [0,1,2,...] fibs = 0 : 1 : zipWith (+) fibs (tail fibs) -- Take only what you need take 5 fibs -- [0,1,1,2,3] takeWhile (<100) fibs -- [0,1,1,2,3,5,8,13,21,34,55,89] -- Rust: lazy iterators (explicit, not transparent) use std::iter::successors; let fibs = successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))) .map(|(a, _)| a); let first_10: Vec<_> = fibs.take(10).collect(); -- Python: generator for Fibonacci def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b import itertools list(itertools.islice(fibonacci(), 10)) # [0,1,1,2,3,5,8,13,21,34]

7.4  Lazy vs Strict Modes

Laziness and strictness are trade-offs:
Property Lazy (Haskell default) Strict (Rust, Python, F# default)
Infinite structures Natural Require explicit iterators
Space usage Unpredictable (thunk chains) Predictable
Short-circuit as library Yes No (requires language support)
Debugging Harder (evaluation order unclear) Easier
Performance Can avoid unneeded work No thunk overhead
Most languages choose strict evaluation with opt-in laziness. Haskell chooses lazy evaluation with opt-in strictness (foldl', seq, ! fields, WHNF forcing). F# is strict but provides Seq (lazy sequences) as a library type.

7.5  Epilogue

Lazy evaluation is a powerful tool when used with awareness of its costs. Infinite streams, demand-driven I/O, and short-circuit abstraction are its most valuable contributions. The explicit iterator and generator patterns in strict languages provide most of the same expressiveness with more predictable resource consumption. The next chapter covers monads - the abstraction that structures sequential computation and effectful operations in functional languages.

7.6  References

Lazy Evaluation - Wikipedia
Lazy Evaluation - Haskell Wiki
Performance and Laziness - Haskell Wiki
Python Generators - Functional HOWTO