Most code is eager: the moment you write down an expression, the machine computes it. Lazy evaluation flips that. A lazy value is a promise to compute later — nothing actually runs until someone genuinely needs the result. "Don't do the work until you have to, and never do it twice."
That sounds like a micro-optimisation, but it unlocks something remarkable: you can describe infinite data — all the natural numbers, an endless stream of random events, the entire Fibonacci sequence — and it costs nothing, because only the pieces you actually ask for are ever built.
The basic tool is a thunk — a zero-argument function wrapping an expression. Writing
() => expensive() doesn't run expensive(); it packages it. The work
happens only when you force the thunk by calling it. Watch the log order: the message
prints only when we force.
The eager square computed before "made eager value" printed; the lazy one waited until we called
lazy(). A thunk is just deferral, but deferral is the whole idea.
Forcing a thunk twice repeats the work. The lazy motto's second half — "never twice" — is fixed by memoising: compute on first force, cache the result, hand back the cache afterward. A closure holds the cache privately.
"working hard..." prints exactly once, no matter how often we force. That memoised thunk is the building block of a lazy stream.
Now the payoff. A stream is a lazy list: a head value plus a
thunk for the rest. Because the tail is deferred, the list can be endless — we only
ever force as far as we walk. Here is the stream of all natural numbers, and a lazy
take that pulls out the first few.
naturals is conceptually infinite, yet the program finishes instantly, because
take forces only five tails and no more. You described an endless object and
used a finite slice of it — the essence of laziness.
Streams let you write a data definition and read off as much as you want. A lazy
map over a stream transforms each element on demand; combine that with recursion and
you get famous one-liners — the Fibonacci sequence defined in terms of itself, or the Sieve of
Eratosthenes as "the head is prime, filter it out of the tail, repeat." In a lazy language like
Haskell you can literally write primes = sieve [2..] over an infinite list. The
machinery is exactly the head-plus-thunk stream above; only the elements you print are ever
computed.
a && b never
evaluates b when a is false. Lazy evaluation makes every
argument behave that way — unused work is never done.Laziness has sharp edges:
length, or a take with no stopping k, tries to build the
whole endless thing. Always consume a finite prefix.() => expensive() forced
in a loop pays the cost every single time — wrap it with a lazyOnce-style cache if
it'll be forced more than once.