100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Lazy Evaluation in Haskell

How Haskell's non-strict evaluation model defers computation until results are needed, enabling infinite structures but requiring care around space leaks.

Practical HaskellIntermediate10 min readJul 10, 2026
Analogies

What Laziness Means in Haskell

Haskell is a non-strict language: an expression is not reduced to a value until something actually demands that value. GHC implements this using thunks -- unevaluated computation graphs that get forced (evaluated to weak head normal form) on first use and then cached so later uses reuse the same result. This differs from eager languages like Python or Java, where let x = f(y) runs f immediately regardless of whether x is ever read.

🏏

Cricket analogy: Like a third-umpire review that's only triggered and computed when the on-field umpire is unsure and the captain asks for it, a Haskell thunk for f y sits idle until x is actually read, unlike a scorer who logs every run the instant it happens.

Thunks, Sharing, and Space Leaks

Because a thunk is shared rather than recomputed, let total = sum [1..1000000] in (total, total) only sums the list once. The flip side is that an unevaluated thunk chain built up inside a lazy foldl can accumulate a huge pile of pending additions in memory instead of a single integer, which is the classic Haskell space leak. Replacing foldl with Data.List.foldl' (strict in the accumulator) or adding a BangPatterns !acc forces each intermediate result immediately and keeps memory flat.

🏏

Cricket analogy: Sharing a thunk is like a stadium scoreboard operator computing the run rate once and reusing it for both the giant screen and radio commentary, but a lazy foldl building up unevaluated addition chains is like letting a full over's run totals pile up unadded until the strategic timeout, causing a scramble at the end.

haskell
-- Lazy foldl builds up a chain of thunks: (((0+1)+2)+3)+...
sumLazy :: [Int] -> Int
sumLazy = foldl (+) 0
-- On a long list this can blow the stack/heap because the
-- additions are never forced until the very end.

import Data.List (foldl')

sumStrict :: [Int] -> Int
sumStrict = foldl' (+) 0
-- foldl' forces the accumulator at each step, keeping memory flat.

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

main :: IO ()
main = do
  print (sumStrict [1..10000000])   -- runs in constant space
  print (take 10 fibs)              -- [0,1,1,2,3,5,8,13,21,34]

Infinite Data Structures

Laziness lets you write fibs = 0 : 1 : zipWith (+) fibs (tail fibs) or primes = sieve [2..] as genuinely infinite lists, because GHC only ever forces as many list cells as take 10 fibs or a bounded consumer actually asks for; the rest of the spine simply stays as an unforced thunk. This turns generate-and-filter algorithms into compositional one-liners that would require explicit termination conditions in a strict language.

🏏

Cricket analogy: A ball-by-ball radio commentary feed is conceptually infinite for the length of a Test match, but the listener only ever consumes the balls actually bowled so far, just as take 10 fibs only forces the first ten cells of an infinite Fibonacci list.

length xs forces the spine of the list (every cons cell) but not the elements themselves, so length (map undefined xs) succeeds even though every element is undefined -- don't assume length proves your list's contents are safe to use.

Controlling Strictness

GHC gives you seq, $!, BangPatterns (f !x = ...), and Control.DeepSeq's deepseq/force to opt back into eager evaluation when profiling shows a leak. seq a b forces a to weak head normal form before returning b, which is enough for primitive types but not for forcing every element of a nested structure, where deepseq is needed instead.

🏏

Cricket analogy: Calling seq is like a captain forcing an immediate DRS decision instead of letting it linger, but deepseq is like reviewing every camera angle and sensor fully before proceeding, forcing the entire nested structure, not just the top-level verdict.

GHC's strictness analyzer already makes many local bindings strict automatically when it can prove doing so is safe and won't change the program's termination behavior, so don't reach for seq or BangPatterns reflexively -- profile with +RTS -s or heap profiling first and only add strictness annotations where a real leak shows up.

  • Haskell is non-strict: expressions become thunks that are only evaluated when their value is actually demanded.
  • Evaluated thunks are shared/memoized, so a value used twice via let is computed once, not twice.
  • Space leaks happen when thunks accumulate unevaluated (e.g., a lazy foldl chain) instead of being forced incrementally.
  • Data.List.foldl', BangPatterns, seq, and deepseq let you opt back into strict evaluation where it matters.
  • Laziness enables infinite data structures like fibs and primes, since only the demanded prefix is ever forced.
  • seq/$! force to weak head normal form (WHNF) only; deepseq forces an entire nested structure.
  • GHC's strictness analysis already optimizes some cases automatically -- profile before manually adding strictness.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#HaskellStudyNotes#LazyEvaluationInHaskell#Lazy#Evaluation#Haskell#Laziness#StudyNotes#SkillVeris#ExamPrep