Lazy Evaluation

Course 1 · Ch 5
Lazy Evaluation
The full treatment haskell1-1 promised — and a genuine scope difference from java2-4's own opt-in Stream laziness

haskell1-1 previewed take 3 [1..] and promised this chapter would explain why it works. Here's the full answer — and it goes well beyond one convenient trick.

What Laziness Actually Means

An expression isn't evaluated until its value is actually demanded. This is the opposite of eager (or "strict") evaluation, which every other language on this site uses by default — C, Java, Python, C#, Rust all evaluate a function's arguments before the function body ever runs. Haskell evaluates an argument only if, and only when, the function body actually inspects it.

Infinite Data Structures, For Real

fibs :: [Integer] fibs = 0 : 1 : zipWith (+) fibs (tail fibs) -- a list defined in terms of ITSELF take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]

fibs is genuinely, structurally infinite and self-referential — it's defined using itself. This is only possible because laziness means the list is never "fully built" anywhere — only as much of it as actually gets demanded is ever computed. take 10 fibs works fine; printing fibs directly would run forever.

undefined and Bottom (⊥) — Errors That Never Surface

let x = undefined in 5 -- evaluates to 5, no error, ever — x is never demanded

haskell1-1's own warn-box previewed this — here's the full explanation. Bottom (⊥) is the formal name for a computation that errors, or loops forever, or is otherwise "unfinished." Because Haskell only forces what's demanded, a bottom value can sit embedded inside a data structure and never actually detonate, as long as nothing ever forces it. This is a real, structural consequence of laziness, not an edge case.

WHNF — Weak Head Normal Form, Briefly

Laziness doesn't mean "left fully unevaluated until printed" — it means evaluated only as far as strictly necessary to make a decision, such as pattern-matching on a constructor. That partial-evaluation point has a real name, weak head normal form (WHNF) — worth knowing the term exists, without needing the full academic definition to use laziness correctly day to day.

Contrasted with java2-4's Stream Laziness

java2-4's own Java Streams are genuinely lazy — intermediate operations don't run until a terminal operation triggers them. But that's an opt-in feature of one specific API; ordinary Java expressions, method calls, and arithmetic are all strictly eager by default, Streams or not. Haskell's laziness applies to every expression in the entire language, all the time, with no separate "lazy API" to reach for — a genuine difference in scope, not just a similarity in spirit.

seq and Forcing Strictness When Needed

x `seq` y -- forces x to WHNF before returning y — a real, honest escape hatch

Laziness isn't free — haskell1-4's own tail-recursion aside foreshadowed this. A long chain of unevaluated computations (called thunks) can build up and eventually need forcing all at once, which can blow the stack. seq forces evaluation to WHNF immediately, on purpose, when a programmer decides laziness is working against them in a specific spot — a real, practical tool, not an admission that laziness was a mistake.

AspectC/Java/Python/C#/RustJava Streams (java2-4)Haskell
Default evaluationeager, everywhereeager, everywhere except Streamslazy, everywhere
Laziness scopenoneone specific API, opt-inthe entire language, always on
Genuinely infinite structuresnot directly possibleStream.iterate() can approximate ita routine, everyday pattern
Use take/head to safely peek at expensive or infinite structures
Rather than trying to force an entire structure to check it, ask for only what's actually needed — take 10 fibs is always safe; forcing all of fibs at once never is.
Laziness can cause space leaks — deferred computation can cost more memory, not less
A long, unevaluated chain of thunks can consume genuinely more memory than the equivalent eager computation would have, since each deferred step has to be remembered until something finally forces it. This is a real, well-documented Haskell-specific gotcha — a leak from deferring computation too long, not from failing to free memory the way c2-2's own manual-memory leaks work.

Coding Challenges

Challenge 1

Define an infinite list of all even numbers using a self-referential or generator-based definition, and use take to print the first 8.

📄 View solution
Challenge 2

Write an expression using let ... in that binds a name to undefined but never actually uses that binding in the final result, and show the expression still evaluates successfully.

📄 View solution
Challenge 3

Write a short comment explaining the real difference in SCOPE between Haskell's laziness and java2-4's own Stream laziness, using a concrete example of ordinary (non-Stream) Java code that would NOT be lazy even though Streams are.

📄 View solution

Chapter 5 Quick Reference

  • Values are computed only when their value is actually demanded — the opposite of every other language on this site's own eager default
  • Self-referential infinite lists (fibs = 0 : 1 : zipWith (+) fibs (tail fibs)) work because nothing forces the whole structure at once
  • undefined embedded but never forced never errors — the formal concept is called bottom (⊥)
  • WHNF is evaluation only as far as necessary to make a decision, not necessarily all the way down
  • Haskell's laziness is the language's own default for everything — a real scope difference from java2-4's opt-in, Streams-only laziness
  • seq forces strict evaluation deliberately — a real escape hatch, not a contradiction of laziness's value
  • Space leaks (too many deferred thunks) are a genuine, distinct Haskell performance gotcha — different in kind from other languages' own memory leaks
  • Next chapter: algebraic data types — the real ancestor of rust1-6's own enums and Option/Result