skip to content

A lazy value holds a thunk: a computation not yet run. What happens the first time it is forced, and every time after?

level: juniorimportance: must knowfreq 58%

answer

  1. a promise of work, not a result
  2. zero arguments, one captured body
  3. demand is what starts it
  4. the cell changes state after evaluation
  5. compare the first demand with the second

basics

~20 s

Forcing runs the stored computation once: the thunk evaluates its body, replaces itself with the resulting value and returns it. Every later force returns that cached value without running anything, so the work is paid for at most once.

solid answer

~40 s

A thunk is a deferred computation held as an object: a zero-argument body plus a slot for the result and a mark saying whether the slot is filled. The first force runs the body, writes the result into the slot, sets the mark and returns the value. Every later force sees the mark, skips the body and returns the stored value, so the body runs at most once however many consumers demand it, and never at all if nobody does. That once-only guarantee is what makes a lazy value safe to share: two readers see the same result, and the expensive part is not repeated. What you pay for it is an allocation per deferred computation and a small check on every read.

code

pseudocode · 11 lines
pseudocode
cell = { evaluated: false, value: none, body: function() return expensiveSum(rows) }

function force(cell)
    if cell.evaluated is false
        cell.value = cell.body()
        cell.evaluated = true
        cell.body = none          // nothing left to run a second time
    return cell.value

force(cell)    // runs expensiveSum once
force(cell)    // reads cell.value, runs nothing

go deeper

for a junior

Be able to say what the object holds before anything demands it: a body to run, not a result. That the first force evaluates and later forces read a cache is the whole first-screen answer.

for a middle

Explain the state change inside the cell - unrun body, then filled slot - and why the implementation overwrites instead of keeping both. Say what a read still costs after forcing: a mark check and a pointer hop.

for a senior

Bring what once-only evaluation buys in production: one expensive computation serving many consumers, at the price of a latency spike on whichever caller happens to demand it first.

for a principal

The call you own is where deferral is the default and where it is opt-in. Deferring every derived value buys skipped work on cold paths and pays allocation plus unpredictable first-touch latency on hot ones.

## What the object holds A **thunk** is a computation stored as data rather than performed. It has three parts: a **body** - code that takes no arguments and produces one value - everything that body needs in order to run, and a **slot** that will eventually hold the result, together with a **mark** saying whether it does yet. Building a thunk costs an allocation and a write. It does not cost the computation, and that is the entire point. *Zero-argument* is the defining shape. A thunk takes no parameters because every input it will ever have was fixed at the moment it was built; nothing remains to be decided, only work remains to be done. That is what separates a deferred computation from an ordinary function: a function is a recipe you may apply to many different inputs, many times over; a thunk is one specific job that has not been started. ## Forcing, step by step **Forcing** is the act of demanding the value. A representative implementation does this, in order: 1. Read the mark. If the cell is already evaluated, return the stored value and stop. 2. Otherwise run the body. 3. Write the result into the slot and set the mark to evaluated. 4. Drop the reference to the body, so the code and everything it needed can be reclaimed. 5. Return the value. Step 1 is why the second demand is cheap. Step 4 is why a successfully forced body cannot run again even in principle: there is nothing left to run. Implementations often describe this as the cell being **overwritten** - the same location that held a description of work now holds the work's result, and readers reach it through exactly the reference they were already holding. | Moment | What the cell holds | What a demand costs | |---|---|---| | Before any demand | an unrun body and what it needs | nothing; nobody has asked | | The first force | the body running, slot about to be filled | the full cost of the computation | | Every later force | the computed value | a mark check and a pointer hop | ## Why the result is cached Caching the forced result is what makes a deferred computation usable *as a value*, and two consequences follow: - **Sharing is safe.** Any number of consumers may hold the same lazy value; whoever demands it first pays, everyone else reads. Without caching, cost would scale with the number of readers instead of with the work. - **The cost is bounded.** The body runs at most once, and possibly zero times. *At most once* is a property you can reason about and budget for; *once per read* is not. An alternative design re-runs the body on every demand and stores nothing. Whether a language defers at all, and which of those two disciplines it applies, is the subject of evaluation strategies rather than of the deferred object itself. What matters here is that the caching step is the difference between a value and a disguised function call. ## What you pay for it Deferral is not free, and a candidate who says it is has only half the model: - **An allocation per deferred value.** The cell is an object on the heap, and for a cheap computation it is often larger than the result it will eventually hold. - **An indirection on every read.** Reaching the value means testing the mark and following a pointer, instead of reading a field directly. - **First-touch latency.** The work does not vanish; it moves to whichever caller happens to demand the value first, which is rarely the caller that created it. - **Retention until the force happens.** Whatever the body still needs in order to run stays alive, because it cannot run without it. ## Where the model usually breaks - **Treating a thunk as work already in progress.** Nothing is running in the background. An unforced thunk is inert; only a demand starts it. - **Assuming deferral reduces total work.** It reduces work only for values that are never demanded. When everything is eventually demanded, laziness performs exactly the same computations plus the bookkeeping. - **Expecting a fresh result on each demand.** Once forced, the value is fixed. A caller who wants a new computation wants a function, not a lazy value. - **Forgetting that an unforced thunk can point at another unforced thunk.** One deferred value is a small fixed cost; a growing chain of them is a memory problem that reads, in the source, like a single number.

  • What happens to the thunk's body after a successful force?
    A typical implementation drops the reference to it, so the body and everything it needed in order to run can be reclaimed. That is both a space saving and a guarantee: with no body left there is nothing that could run a second time, and keeping it beside the finished value would cost memory for a computation that can never legitimately happen again.
  • If nothing ever forces a lazy value, what did the program pay?
    The allocation for the cell and the write that installed it - not the computation. That is the whole bet of deferral: spend a small fixed cost per value in order to avoid a large variable one, and win whenever demand is unlikely or the body is expensive.
  • Does forcing the same lazy value from two places run the body twice?
    No. Both demands reach the same cell, and only the first finds it unevaluated; the second sees the mark and returns the stored result. That is exactly why a lazy value is safe to hand to several consumers, and why the cost of the body does not scale with the number of readers.

saying these in an interview costs you the question

  • Says a thunk stores the finished value rather than the unrun computation.
  • Thinks every read of a lazy value re-runs its body.
  • Confuses a deferred computation with work already running in the background.
  • Claims laziness is free because nothing is allocated until the value is forced.
  • Believes forcing hands back another thunk that must be forced again.