Haskell hands you an unevaluated "I owe you a value" — a thunk — and only forces it when something actually needs it. That's why infinite lists are ordinary, and why the occasional space leak sneaks in. Scala's lazy val / LazyList is the opt-in version of this default.
In Scala, evaluation is eager by default and lazy when you ask. You write val x = expensive() and it runs now; you write lazy val x = expensive() and it waits. Haskell flips the default: everything waits. An expression isn't a value — it's a recipe for one, held until somebody demands the result.
That single design choice ripples through the whole language. This chapter walks it end to end:
[1..], repeat, cycle, and the self-referential fibs.take, foldr, and && consume only what they need.foldl' / seq / $! / bang patterns.Every expression in Haskell starts life as a thunk — a deferred computation, a little promise that says "I'll be a value when you need me." Nothing inside it runs until something forces it.
You already know this mechanism from Scala; you just had to opt in. Compare:
// Scala — eager unless you say otherwise
val a = expensive() // runs NOW
lazy val b = expensive() // waits until first use
In Haskell there's no keyword. let a = expensive () already behaves like Scala's lazy val: the work is parked until a is demanded — and if it never is, it never runs.
This is why a function can take an argument it never looks at and that argument's value is never computed — not even if computing it would loop forever or throw. The classic const x _ = x can be handed undefined as its second argument and shrug it off.
lazy val everywhere, plus automatic memoization. Once a thunk is forced, its result is cached — force it again and you get the stored value, not a re-run.Here's the part that trips people up: forcing a value usually does not evaluate it all the way down. It stops at the outermost constructor — a state called weak head normal form, or WHNF.
Take the pair (1+1, 2+2). Force it to WHNF and you learn one thing: it's a pair, (_, _). The tuple constructor is exposed — but its two slots are still unevaluated thunks. Nobody added anything yet.
(⟨1+1⟩, ⟨2+2⟩): the pair exists, insides deferred. A list forced to WHNF tells you only whether it's [] or (x : xs) — nothing about x or how long it is.(2, 4).Most of the time WHNF is all the demand that propagates — pattern-matching on a constructor forces just enough to see which constructor it is. Step it through below.
Once values are deferred, an infinite list stops being a paradox. It's just a list whose tail is a thunk that, when forced, produces one more element and another thunk. You only ever build the prefix you actually pull on.
[1..] — every integer from 1 upward. Scala's LazyList.from(1).repeat 0 — [0, 0, 0, …] forever.cycle [1,2,3] — [1,2,3,1,2,3,…], the list looped end to end.The showstopper is the self-referential definition of the Fibonacci numbers:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Read it slowly. fibs starts 0, 1, …, and each later element is the sum of the two before it — computed by zipping fibs against its own tail. It refers to itself while being defined, and that only works because the tail is lazy: each element is forced into existence exactly when something asks for it. Pull on it below.
take and foldr can stop earlyIf [1..] is infinite, how does take 5 [1..] ever return? Because demand flows backwards: take 5 asks for exactly five elements, so exactly five thunks get forced. The rest of the infinite list is never built.
take 5 (map expensive [1..]) -- only maps over 5 elements
The same logic powers short-circuiting. In Haskell && and || are ordinary functions, lazy in their second argument — and that falls out of laziness, you don't need special syntax for it:
False && undefined -- => False, never touches undefined
True || undefined -- => True, same deal
And foldr can quit before consuming its whole input. Because it associates to the right, the combining function gets the rest of the fold as a lazy argument — so if it ignores that argument, the fold stops:
any p = foldr (\x acc -> p x || acc) False
any even [1..] -- => True, stops at the first 2
That's the lazy advantage in miniature: foldr over an infinite list can terminate, as long as the consumer stops demanding.
Laziness has a bill, and it arrives as memory. If you keep building thunks without ever forcing them, they pile up — a chain of deferred work that quietly eats the heap. That's a space leak.
The textbook example is a strict-looking sum that isn't:
foldl (+) 0 [1..10000000] -- builds a giant thunk, may blow the stack
You'd expect this to tick along holding a single running total. It doesn't. Lazy foldl never forces the accumulator as it goes, so instead of a number it grows a nested expression: ((((0+1)+2)+3)+…), ten million layers deep. Only when the final result is demanded does that whole tower collapse — and forcing it can overflow the stack or exhaust memory first.
The accumulator looks like a number, but until something forces it, it's a tree of deferred additions. Watch the chain grow below.
The fix is to force the accumulator as you go, so each step collapses to a value instead of stacking another thunk. Haskell gives you several tools for that — each one a way to say "evaluate this to WHNF, now."
foldl' — the strict left fold, from Data.List. It forces the accumulator to WHNF at every step. This is the one-line cure for the foldl leak — reach for it by default.seq a b — forces a to WHNF before returning b. It's how you say "make sure this is evaluated before moving on."f $! x — strict application: force x to WHNF, then apply f. Just seq wearing an operator hat: f $! x = x `seq` f x.BangPatterns extension, !x in a pattern forces x when the pattern matches. Common in tight accumulator loops: go !acc (x:xs) = go (acc+x) xs.Here's the strict accumulator written by hand, the bang-pattern way:
sumStrict = go 0
where go !acc [] = acc
go !acc (x:xs) = go (acc + x) xs
When to force? Not everywhere — premature strictness throws away the benefits laziness gives you. The practical rule: force the accumulator of a left fold or loop, and otherwise leave things lazy until a profiler tells you a thunk is piling up.
seq only forces to WHNF, not all the way down. seq (1+1, 2+2) () forces the pair to (_, _) but leaves the additions as thunks. To force deeply you need deepseq (from the deepseq package) — but you rarely want it.It's easy to read sections 5 and 6 and conclude laziness is a liability. It isn't — it's the thing that makes a lot of clean Haskell possible. Three places it pays off:
when(cond)(body) evaluates body eagerly unless you mark the parameter => Boolean (by-name). In Haskell every argument is by-name for free, so you can define if-like and looping combinators as plain functions, no macro or by-name annotation needed.take 10 primes needs no shared bound between the two halves — the consumer's demand is the bound.take 3 . filter even . map (*2) $ [1..] walks the list once and stops after three results. No intermediate full lists are ever built, even though each stage reads like it processes everything.The trade is real but learnable: laziness buys you composition and infinite data; you pay it back with the occasional foldl'. Knowing which way demand flows is the whole skill.
Scala's LazyList and lazy val are the opt-in version of Haskell's everyday default. Side by side, the infinite naturals and the self-referential fibs:
You can now read demand the way a Haskell compiler does: thunks until forced, WHNF as the usual depth, infinite data as routine, and foldl' when the thunks pile up. Next we stack effects — combining Reader, Except, and IO into one workable monad without drowning in boilerplate.