Lazy Evaluation
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 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
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
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.
| Aspect | C/Java/Python/C#/Rust | Java Streams (java2-4) | Haskell |
|---|---|---|---|
| Default evaluation | eager, everywhere | eager, everywhere except Streams | lazy, everywhere |
| Laziness scope | none | one specific API, opt-in | the entire language, always on |
| Genuinely infinite structures | not directly possible | Stream.iterate() can approximate it | a routine, everyday pattern |
take 10 fibs is always safe; forcing all of fibs at once never is.
c2-2's own manual-memory leaks work.
Coding Challenges
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 solutionWrite 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 solutionWrite 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 solutionChapter 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