Thunk or Lazy Promise¶
Deferral primitive (tool) — instantiates Demand-Triggered Deferred Evaluation
Packages an unevaluated computation as a first-class value that runs at most once when forced — relocating its side effects and failures to force-time.
A thunk (or lazy promise) packages an unevaluated computation as a first-class value: a small object you can pass around, store, and return, holding the code and captured inputs needed to produce a result but not having produced it yet. Forcing the thunk runs the computation; a lazy promise additionally memoizes — it runs at most once, caches the result, and returns that cached value on every later force. What makes it THIS mechanism is that it makes the deferred unit itself into an ordinary value, which in turn relocates two things in time: the computation's side effects and its failures fire at force-time, wherever and whenever that happens, not where the thunk was created.
Example¶
A config object exposes databaseConfig = lazy { parseFile("db.yaml") }. Building the config object touches no disk — the thunk just holds the closure. The file is read the first time some code forces databaseConfig, which might be deep inside a request handler minutes later and modules away. If db.yaml is missing, the exception is thrown there, at the force site in the request handler, not at startup where the config was built — so the stack trace points at a caller that has no idea it triggered a file read. Because it is a memoizing promise, a second force does not retry: it returns the same cached value (or re-raises the same cached exception).
That relocation is the whole double-edged point. You paid nothing at construction — but you moved the failure to a place that never expected to do I/O.
How it works¶
- Capture, don't run. Wrap the computation and the inputs it closes over into a value; construction does no work.
- Force to realize. The first force evaluates the computation and produces the result.
- Memoize the outcome. A lazy promise stores the result (or the raised exception) and returns it on every subsequent force — the work happens at most once.
- Relocate effects and failures to force-time. Any I/O, mutation, or exception in the computation occurs when and where forcing happens, not at construction.
Tuning parameters¶
- Memoize vs. re-evaluate — cache the first result (lazy promise) vs. recompute each force (bare thunk). Memoizing shares one value; recomputing re-runs the side effects each time.
- Exception caching — whether a thrown result is cached and re-raised, or each force retries. Caching makes failure deterministic; retrying allows recovery but re-runs the effects.
- Force thread-safety — whether concurrent first-forces may run the computation twice. Synchronizing guarantees once-only at a locking cost.
- Strictness override — a way to force eagerly at a chosen safe point, pulling the failure or effect back to where it is expected.
When it helps, and when it misleads¶
Its strength is expensive-or-optional values that may never be used, breaking initialization-order cycles (a value can refer to one not yet built), and turning "compute this later" into something you can pass around like any other value.
It misleads through that same relocation of effects and failures. A thunk that throws does so far from its construction, making errors hard to trace; captured inputs are pinned alive until the thunk is forced or dropped, which is a quiet space leak; and a bare, non-memoizing thunk forced twice runs its side effects twice. The classic misuse is wrapping side-effecting work in a lazy value for the look of cleanliness and then being unable to reason about when the effect fires. The discipline is to keep thunks pure where possible and to force eagerly at a known-safe point whenever the timing of an effect or failure matters.[1]
How it implements the components¶
Thunk or Lazy Promise fills the deferred-value core of the archetype — the unit itself and its force-time semantics — not sequence pull or demand detection:
deferred_work_unit— the thunk is the first-class deferred unit: a suspended computation packaged as a value.result_sharing_policy— force-once-and-share: a lazy promise memoizes its result so later forces return the cached value.side_effect_boundary— the computation's side effects fire at force-time, not construction-time.failure_and_exception_semantics— an exception surfaces at force-time, and, if memoized, may be cached and re-raised.
It does not produce a sequence of values incrementally on each pull — that is the Lazy Sequence Generator — nor wrap the deferral behind a transparent object that looks like the real thing, which is the Lazy-Initialization Proxy.
Related¶
- Instantiates: Demand-Triggered Deferred Evaluation — the computation is realized only when the thunk is forced.
- Sibling mechanisms: Call-by-Need Evaluator · Lazy Sequence Generator · Lazy-Initialization Proxy · Memoization · Short-Circuit Evaluation
References¶
[1] The term thunk dates to the implementation of call-by-name parameter passing in Algol 60, where an unevaluated argument was compiled into a small routine — a thunk — that ran when the parameter was used. A memoizing thunk is the standard implementation of call-by-need. ↩