Haskell · concepts · evaluation
Lazy evaluation in Haskell, explained
Most languages are strict: an expression is evaluated as soon as it's bound to a name. Haskell is different — it is lazy (more precisely, non-strict) by default: an expression is not evaluated until its result is genuinely needed. This one design choice is behind some of Haskell's most distinctive powers and its most notorious trap. This guide explains what lazy evaluation is, how thunks work, what it buys you, and how to turn it off when you need to.
The idea in one sentence
Under lazy evaluation, binding a value doesn't compute it — Haskell stores a deferred computation (a thunk) and only forces it when another computation demands the result. If the result is never demanded, the work is never done.
Thunks: deferred computation
When you write let x = expensive 42, Haskell does not run expensive. It creates a thunk — an unevaluated promise of expensive 42. The thunk is forced only when something needs x's actual value (for example, printing it or pattern-matching on it). Force it once, and the result is cached so it isn't recomputed.
let x = expensive 42 -- nothing runs yet; x is a thunk
let y = x + 1 -- still nothing; y is a thunk too
print y -- NOW x and y are forced and evaluated What laziness buys you
- Infinite data structures.
[1..]is the infinite list of integers.take 5 [1..]returns[1,2,3,4,5]because only the first five are ever demanded. - Composition without waste. You can chain
filterandmapover a huge list and Haskell fuses the work, producing only what the consumer pulls — no intermediate full lists if they're never fully demanded. - Short-circuiting for free.
False && undefinedreturnsFalsewithout ever touchingundefined, because the second argument is never forced. - Self-reference. Classic one-liners like
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)define the infinite Fibonacci stream — only possible because the structure is built lazily.
The trap: space leaks
Laziness has a cost. Unevaluated thunks accumulate in memory, and a long chain of deferred computations can quietly balloon heap usage — a space leak. The textbook case is a lazy left fold that builds a giant tower of (((0 + 1) + 2) + 3) ... thunks instead of adding as it goes:
foldl (+) 0 [1..1000000] -- builds a million thunks, may blow the stack
foldl' (+) 0 [1..1000000] -- strict fold: adds as it goes, constant space The fix is to force evaluation where it matters. Reach for the strict left fold foldl' (from Data.List) for accumulations.
Forcing strictness: seq, $! and BangPatterns
Haskell gives you precise tools to demand evaluation:
seq a b— forcesato weak head normal form before returningb.($!)— strict application:f $! xforcesxbefore applyingf.BangPatterns— the{-# LANGUAGE BangPatterns #-}extension lets you writelet !x = ...to force a binding.
A subtlety worth knowing: forcing typically reaches weak head normal form (the outermost constructor), not full evaluation. To force deeply, use force / deepseq from the deepseq package.
FAQ
Is Haskell lazy or non-strict? Technically non-strict (a semantic guarantee); GHC implements it via lazy evaluation with thunks and sharing. In practice people say "lazy".
Why does my Haskell program use so much memory? Often a space leak from accumulated thunks — commonly a lazy foldl. Switch to foldl' or add strictness with seq/BangPatterns.
What is a thunk? An unevaluated, deferred computation stored in place of a value, forced only when the value is demanded and then cached.
Does laziness make Haskell slow? Not inherently — it can avoid needless work and enable fusion. But careless laziness causes space leaks; knowing when to force is part of writing good Haskell.
Laziness is the same machinery that makes Haskell's other abstractions compose cleanly — see how sequencing works in our guide to monads in Haskell, and set up the toolchain to try these examples in GHCi with the GHC compiler guide.