Polyglot
Languages Haskell laziness
Haskell § laziness

Laziness

Haskell is non-strict (lazily evaluated) by default: a subexpression is evaluated only when its value is required. The mechanism is one of the language’s most distinctive features and one of its most consequential. Laziness admits infinite data structures, modular composition of generators and consumers, and short-circuiting algorithms that require no special-case logic. It also admits space leaks — programs that retain memory the developer expected to be reclaimed — and produces evaluation order subtleties that the developer must understand to write efficient code. The conventional discipline is to use laziness where it helps and to introduce strictness explicitly where it does not.

This page covers the non-strict semantics, weak head normal form, the strictness primitives (seq, deepseq, bang patterns), space leaks and the conventional defences, and the patterns for working effectively with lazy evaluation.

The non-strict semantics

In a strict language, function arguments are evaluated before the function is called. In Haskell, arguments are evaluated only when their values are required:

ignoreFirst :: a -> b -> b
ignoreFirst _ y = y

result = ignoreFirst (error "boom") "hello"
-- "hello" — the error is never raised because the first argument is never used

The error call would terminate the program if evaluated; lazy evaluation never forces it.

The mechanism extends throughout the language:

  • if cond then a else ba is evaluated only if cond is True; b only if cond is False.
  • case x of {pat1 -> e1; pat2 -> e2}x is evaluated to determine the matching pattern; the unselected arm is not.
  • let x = e1 in e2e1 is evaluated only when x is used in e2.
  • (:) head tail — neither head nor tail is evaluated until the resulting list is consumed.

The && and || operators, which short-circuit in C and Java, do so in Haskell because of the same underlying laziness — there is no special case in the operator definition.

Thunks and weak head normal form

Internally, Haskell represents un-evaluated expressions as thunks — closures that, when forced, compute the value and replace themselves with the result. The mechanism admits sharing: if the same expression is referenced from multiple places, the thunk is forced once and the result is shared.

let x = expensive_computation
in (x, x)        -- expensive_computation runs once, both pair elements share the result

When a value is forced, it is evaluated to weak head normal form (WHNF) — the outermost constructor is determined, but the constructor’s arguments may still be thunks:

let pair = (1 + 2, 3 + 4)
forceIt = case pair of (_, _) -> "ok"
-- forceIt is "ok"; the pair has been forced to WHNF, but
-- 1 + 2 and 3 + 4 inside the pair remain as thunks

To force a value to normal form (fully evaluated, all the way down), use deepseq (treated below).

The WHNF/normal-form distinction matters for performance: forcing to WHNF is enough for many operations (pattern matching, the seq primitive), but reductions over the whole structure require deeper evaluation.

seq and forced evaluation

The seq primitive forces its first argument to WHNF and returns its second:

seq :: a -> b -> b

The semantics: seq x y is equivalent to y if x terminates, and to (bottom; non-termination or error) if x does not terminate. The mechanism admits introducing strictness explicitly:

strictPair :: a -> b -> (a, b)
strictPair x y = x `seq` y `seq` (x, y)
-- both x and y are forced before the pair is constructed

The seq mechanism is the foundation of strictness control in Haskell.

The ($!) operator is seq-based strict application:

($!) :: (a -> b) -> a -> b
f $! x = x `seq` f x

length $! computeList     -- forces computeList to WHNF before length is called

The conventional uses are forcing accumulator updates in folds and forcing intermediate results in long pipelines.

Bang patterns

The BangPatterns extension admits strict patterns — patterns that force their argument when the pattern is matched:

{-# LANGUAGE BangPatterns #-}

sum' :: Num a => [a] -> a
sum' = go 0
  where
    go !acc []     = acc
    go !acc (x:xs) = go (acc + x) xs

The !acc pattern forces acc to WHNF before the body runs. The mechanism is the conventional defence against thunk accumulation in tail-recursive functions:

-- Without bang: acc accumulates thunks
go acc []     = acc
go acc (x:xs) = go (acc + x) xs

-- With bang: acc is evaluated each iteration
go !acc []     = acc
go !acc (x:xs) = go (acc + x) xs

Without the bang, go 0 [1..1000000] produces a thunk like ((((0 + 1) + 2) + 3) + ... + 1000000) — a chain of 1,000,000 unevaluated additions, which is reduced when the result is finally forced. The chain may consume more memory than the input list. With the bang, acc is reduced after each step, and the program runs in constant space.

deepseq and full forcing

The deepseq package provides NFData — types that admit reduction to normal form (fully evaluated):

import Control.DeepSeq

deepseq :: NFData a => a -> b -> b
($!!)   :: NFData a => (a -> b) -> a -> b
force   :: NFData a => a -> a

The mechanism admits forcing a value all the way to normal form:

result <- (return $!! computeBigList) :: IO [Int]
-- computeBigList is fully evaluated before result is bound

The NFData class has standard derivation:

import Control.DeepSeq (NFData)

data Big = Big Int [Int] String
    deriving (Generic, NFData)        -- with DeriveGeneric extension

deepseq is the conventional choice when partial evaluation (WHNF) is insufficient — for example, when storing data in a strict-fielded structure or sending data across a thread boundary.

Strict data fields

A data declaration may mark fields as strict with !:

data Pair a b = Pair !a !b

-- Equivalent to:
data Pair a b = Pair a b   -- with a `seq` on construction

When a Pair is constructed, both a and b are forced to WHNF before the pair is created. The mechanism is the conventional way to ensure values are evaluated when stored.

The StrictData language extension makes all data fields strict by default:

{-# LANGUAGE StrictData #-}

data Pair a b = Pair a b   -- both fields strict

The Strict extension makes everything strict (all bindings, all fields); it is rarely used and admits substantial behavioural changes from the standard.

Infinite data structures

The most distinctive consequence of laziness: programs may construct infinite data structures and consume only as much as they need:

naturals :: [Int]
naturals = [0..]

squares :: [Int]
squares = map (^2) naturals

firstFive :: [Int]
firstFive = take 5 squares     -- [0, 1, 4, 9, 16]

The naturals list is conceptually infinite; squares is also infinite; firstFive is finite — but no element beyond the fifth is computed. The laziness admits expressing the algorithm without explicit termination logic.

The iterate and repeat functions construct infinite sequences:

iterate :: (a -> a) -> a -> [a]
iterate f x = x : iterate f (f x)

powers :: [Int]
powers = iterate (* 2) 1   -- [1, 2, 4, 8, 16, ...]

repeat :: a -> [a]
repeat x = x : repeat x

The conventional pattern for infinite sequences combines a generator (that produces infinitely) with a consumer (that takes only what it needs):

fibs :: [Int]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

firstTen :: [Int]
firstTen = take 10 fibs    -- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

The mechanism is the foundation of much of Haskell’s elegance.

Memoisation through laziness

Lazy evaluation admits a form of automatic memoisation: values shared via let are computed once and shared:

let expensive = computeFibonacci 1000
in (expensive, expensive)
-- computed once, used twice

The pattern admits direct expression of memoised recursive functions:

memoFib :: Int -> Int
memoFib = (cache !!)
  where
    cache = 0 : 1 : zipWith (+) cache (tail cache)

The cache is an infinite list of Fibonacci numbers; each access reuses the already-computed prefix. The combination of laziness and sharing admits memoisation without explicit caching infrastructure.

Short-circuiting and modular composition

Lazy evaluation admits operations that take “as much as needed”:

any p = foldr ((||) . p) False
-- ((p x1 || (p x2 || (p x3 || ... )))) — short-circuits on the first True

find p xs = case filter p xs of
    []    -> Nothing
    (x:_) -> Just x
-- filter is lazy: only enough is generated to find the first match

The any, all, find, take functions all rely on laziness for short-circuiting; without it, the operations would consume the entire input regardless of whether the result has been determined.

Space leaks

The principal pitfall of lazy evaluation: space leaks — programs that retain memory by holding thunks the programmer expected to be evaluated.

The accumulator anti-pattern

-- Lazy: thunks accumulate
sum :: Num a => [a] -> a
sum = foldl (+) 0    -- foldl is lazy

-- The accumulator becomes ((((0 + 1) + 2) + 3) + ...)
-- All elements remain in memory until forcing

The conventional defence is foldl' (strict left fold) from Data.List:

import Data.List (foldl')

sum :: Num a => [a] -> a
sum = foldl' (+) 0    -- strict in the accumulator

foldl' forces the accumulator at each step; the result runs in constant space.

Lazy state in records

data Counter = Counter Int    -- field is lazy

incr :: Counter -> Counter
incr (Counter n) = Counter (n + 1)

step1000 = iterate incr (Counter 0) !! 1000
-- Counter (0 + 1 + 1 + 1 + ...) — 1000 unevaluated additions

The defence is to make the field strict:

data Counter = Counter !Int

incr :: Counter -> Counter
incr (Counter n) = Counter (n + 1)
-- now the addition is forced when Counter is constructed

Deep accumulation

For complex accumulators (records, lists, maps), foldl' (+) is not enough — the internal structure may also be lazy. The defence is deepseq-based forcing or strict data declarations.

The conventional contemporary discipline:

  • Use foldl' for numeric and simple accumulators.
  • Use strict data fields for accumulator state.
  • Profile suspicious code with -rtsopts -prof and +RTS -p to identify space leaks.
  • Use the nothunks library for testing that values are fully evaluated.

When laziness helps

Lazy evaluation is particularly valuable for:

  • Modular composition: a generator and a consumer can be developed independently and combined without explicit coordination of how much is produced.
  • Short-circuiting: operations that examine only as much as needed (like any, find, &&, ||).
  • Infinite data structures: streams, traces, lazy trees.
  • Memoisation through sharing: computed-once-used-many idioms.
  • DSL embedding: building up an AST without forcing premature evaluation.

When laziness hurts

Lazy evaluation introduces costs:

  • Per-thunk overhead: each unevaluated computation allocates a small heap object.
  • Space leaks: accumulating thunks consume memory.
  • Unpredictable performance: when does the work actually happen?
  • Debugging difficulty: stack traces show the evaluation point, not the source of the work.
  • Poor interaction with strict-by-default subsystems: numeric arithmetic, IO, file handles.

The conventional contemporary advice is to use laziness where it helps and to introduce strictness explicitly where it does not.

Strict alternatives in the standard library

For numeric folds, Data.List.foldl' is strict:

import Data.List (foldl')

total = foldl' (+) 0 list

For strict maps, Data.Map.Strict:

import qualified Data.Map.Strict as Map

m = Map.fromList [(k, v) | k <- keys, v <- values]
-- strict in the values

Data.Map.Lazy (the default Data.Map) is lazy in values; Data.Map.Strict is strict.

For strict text, Data.Text:

The text package provides both Data.Text (strict) and Data.Text.Lazy (lazy); for most purposes the strict variant is the right choice.

Common patterns

Strict accumulator

{-# LANGUAGE BangPatterns #-}

import Data.List (foldl')

countMatching :: (a -> Bool) -> [a] -> Int
countMatching p = go 0
  where
    go !n []     = n
    go !n (x:xs) = go (if p x then n + 1 else n) xs

The bang pattern on n ensures the accumulator is forced each iteration.

Strict data fields for hot types

data Stats = Stats !Int !Double !Double
    -- (count, sum, sum-of-squares)
    -- all fields strict; the accumulator can update each step

The pattern is the conventional choice for accumulator-style records.

Lazy generator + strict consumer

let xs = filter heavy . map transform $ items
result = sum xs              -- the strict sum forces just enough

-- Or, even better:
let !result = sum (filter heavy . map transform $ items)
-- the bang ensures result is forced before being returned

The combination admits a clean compositional pipeline that nonetheless evaluates eagerly when the final result is forced.

force for full evaluation at a boundary

import Control.DeepSeq (force)

data Snapshot = Snapshot ![Int] !Map.Map.Map

createSnapshot :: IO Snapshot
createSnapshot = do
    items <- loadItems
    table <- buildTable items
    return $ force (Snapshot items table)
    -- everything is fully evaluated before the snapshot escapes

The pattern admits “this value is known to be fully evaluated when it leaves this function” — useful for cross-thread data and for releasing references to upstream computation.

A note on the trade-off

Laziness is the principal feature that distinguishes Haskell from most other functional languages. The trade-off:

  • Lazy by default with explicit strictness (Haskell): admits infinite data, short-circuiting, modular composition. Costs: space leaks, unpredictable performance.
  • Strict by default with explicit laziness (most other functional languages): predictable performance, easier debugging. Costs: more elaborate plumbing for streams and infinite structures.

The Haskell community has converged on a discipline that uses lazy evaluation for the patterns where it shines and explicit strictness for performance-critical numeric work. Reading and writing efficient Haskell requires fluency with both; the conventional contemporary advice is to enable -Wall (which warns about some space-leak-prone patterns), use foldl' over foldl, mark accumulator fields strict, and profile suspicious code.