Lesson 28 · Eager vs lazy evaluation

Eager vs Lazy Evaluation

Most languages you use day-to-day — Python, TypeScript, Scala, Java — evaluate arguments before passing them to a function. This is eager evaluation (also: strict evaluation). A few languages — Haskell, Clojure’s lazy-seq, Racket’s delay — defer evaluation until the value is demanded: lazy evaluation.

The difference matters as soon as you care about: (a) avoiding unnecessary work, (b) short-circuit logic, (c) infinite structures (coming in Lesson 29).

Terms:

  • Eager / strict evaluation: an argument expression is evaluated before the function call. By the time the function body runs, all argument values are computed.
  • Lazy / non-strict evaluation: an argument expression is evaluated when and only when its value is demanded — possibly never.
  • Thunk: an explicit encoding of a lazy value in an eager language — a zero-argument lambda: lambda: <expr>. Creating the thunk is cheap; the expression runs only when you call thunk().

Part 1 — Predict: what does always_first do?

def always_first(x, y):
    return x

always_first(42, 1 / 0)   # ?

In Python (eager): what happens, and when does it happen? (Before the function body runs, or inside it?)

If Python were lazy: what would happen instead?


Part 2 — Manual thunking

Python gives you an escape hatch: wrap the risky argument in a lambda.

def always_first_lazy(x, y_thunk):
    return x   # never calls y_thunk()

always_first_lazy(42, lambda: 1 / 0)   # ?

Why does wrapping in lambda: prevent the error?


Part 3 — Short-circuit as hidden laziness

Python’s and and or are not regular functions:

False and (1 / 0)   # → False, no error
True  or  (1 / 0)   # → True,  no error

But a custom my_and function isn’t lazy:

def my_and(a, b):
    if a: return b
    return False

my_and(False, 1 / 0)   # ?

What happens, and why does and (the built-in operator) succeed where my_and fails? Fix my_and using a thunk so it short-circuits correctly.


Part 4 — Lazy vs eager on a non-terminating argument

def f(x):
    return 1

f(loop_forever())   # loop_forever() never returns

In an eager language: what happens when you call f(loop_forever())? In a lazy language: what happens?


The question above

The MCQ at the top of this puzzle asks about evaluation order — a specific, testable consequence of Python’s eager semantics. Think it through before reading the solution.

After answering, confirm your mental model against Parts 1–4.

In Python, which print fires first? ```python def trace(label, x): print(label) return x result = trace('A', 1) + trace('B', 2) ```