skip to content

Laziness & Deferred Evaluation

Computing a value only when it is demanded, which is what makes unbounded sequences, early exit and single-pass pipelines possible. Interviewers probe what a pipeline does before its terminal step.

on this pageshow

explore

questions

20

A retry helper builds a never-ending sequence of back-off delays, then takes the first five - why does building it not run forever?

level: juniorimportance: must knowfreq 62%

answer

  1. a description, not a collection
  2. nothing is computed at definition
  3. a seed plus a next-element rule
  4. elements appear only on demand
  5. the prefix length decides the work

basics

~20 s

An unbounded sequence is a recipe, not a collection: a seed plus a rule for the next element, computed only when something demands it. Building it stores the rule; taking five runs the rule four times.

solid answer

~40 s

An endless sequence is a **producer**, not a container. What the program holds is a seed - the first delay - and a step rule that turns any element into the one after it. Writing that definition allocates the rule and nothing else; zero delays exist yet. A take-first-five step then pulls one element at a time: the seed is given, the rule runs four times, and the pull stops. Element six is never produced, because nothing asked for it. The sequence is "endless" only in the sense that the rule carries no stopping condition; how far it is ever driven is decided entirely at the consuming end. That is also why the same definition is safe to reuse - each consumer drives it as far as it needs and no further.

code

pseudocode · 11 lines
pseudocode
function next_delay(previous):
    return previous * 2

// a description: a seed, and how to get the element after any element
delays = sequence(seed = 100, step = next_delay)

// nothing has run yet - no delay exists except the seed

first_five = take(delays, 5)
// pulls five times: 100, 200, 400, 800, 1600
// next_delay ran four times; element six was never produced

go deeper

for a junior

Recall the one-line reason: an endless sequence is a rule for making the next element, so writing it down computes nothing, and the work stops once the prefix the caller asked for is filled.

for a middle

Explain the pull. The take step demands elements one at a time, each demand costs one application of the step rule, and the definition itself never contains a stopping condition.

for a senior

Be able to say which operations over such a producer are safe in a running service, and to spot the ones that quietly drive it toward exhaustion instead of toward a bounded prefix.

for a principal

Weigh whether an unbounded producer should cross a module boundary at all: callers gain the freedom to choose their own bound, and gain a way to hang a thread that a bounded return value would not have handed them.

## A description, not a collection An unbounded sequence is usually introduced as "a list of all the doubling delays", which is exactly the picture that makes it sound impossible. Nothing in a running program holds infinitely many values. What the program holds is a **producer**: a **seed** - the first element - and a **step rule** that maps any element to the one after it. The retry helper holds the number `100` and the rule "double the previous delay". That pair is small, fixed-size data. It would be exactly the same size if the rule stopped after three elements, because a stopping condition is not part of it. This is what people mean when they call a lazy sequence a **recipe**. A recipe that says "keep adding stock and simmering" is a few lines long no matter how long anybody simmers. Reading it costs nothing; cooking costs what you cook. ## Why the definition runs nothing Under **non-strict** (lazy) evaluation an element is computed when something **demands** it, and not before. Three consequences follow, and together they are what makes an endless definition safe to write: - **Construction is constant in time and space.** Building the producer stores a seed and a rule. No element after the seed exists. - **There is no length to compute.** A strict collection knows its size because it holds its elements; a producer knows only how to make one more. - **The producer never asks whether it should continue.** It has no terminating case and needs none - it is only ever asked for the next element, one at a time. ## Where the work actually happens The consuming end drives everything. A take-first-five step demands element one, then two, and stops the instant it has five. Traced against a seed of 100 and a doubling rule: | demand | what the rule does | elements so far | |---|---|---| | first element | nothing - the seed is given | 100 | | next element | 100 x 2 | 100, 200 | | next element | 200 x 2 | ..., 400 | | next element | 400 x 2 | ..., 800 | | next element | 800 x 2 | ..., 1600 | Five elements cost **four** applications of the rule, because the seed is given rather than computed. Then the pulling stops: the take step has what it asked for and demands nothing more, so element six is never produced and never existed. ## Who decides how far it goes The producer has no opinion about where the sequence ends; the **consumer** decides, by how much it pulls. That is a design property, not a technicality: 1. **One definition serves many policies.** One caller retries three times, another keeps going while the delay stays under five seconds, a third stops when a deadline passes. All three drive the same producer. 2. **The bound lives where the knowledge lives.** The code that knows the retry budget sets it, instead of a shared helper guessing a number on everyone's behalf. 3. **Reuse is interference-free** for a pure producer: each consumer drives the same rule from the same seed and sees the same elements, because the rule is a function of the previous element, not of a shared position that someone else can advance. ## The obligation it hands you Two things must hold for a bounded pull to finish, and a producer that breaks either one is not safe merely because it is lazy: - **Each element must be produced in finite time.** A step rule that loops, or that itself demands an unbounded number of elements, hangs on the very first demand. - **Something must bound the demand.** Laziness makes an endless definition harmless; it does nothing about a consumer that keeps asking. A step that has to reach the end of the sequence before it can answer will pull until the process dies. The first half is the screening answer - defining costs nothing. The half that catches people in production is the second: **cheap to define is not cheap to consume**. The same three lines that cost nothing to write will pin a core at full load the moment something downstream asks for a total, a maximum or a sorted copy of them.

  • What has to be true of the step rule for a take-first-n to terminate?
    Every application of it must finish in finite time, and it must not itself demand an unbounded number of elements from somewhere else. Laziness only defers work; it does not rescue a rule that loops on its first invocation. If one application hangs, the very first demand hangs, and the fact that the caller asked for only five elements changes nothing.
  • If two consumers each take a prefix of the same endless definition, do they interfere with each other?
    Not for a pure producer. The rule is a function of the previous element, so each consumer drives it from the same seed and sees the same elements; neither can consume the other's. A producer that carries a mutable cursor instead is a different animal - one consumer advances the position and the other picks up the tail, which is a stateful iterator rather than a lazy description.

A knitting pattern that says "repeat this row forever" is one page of instructions, not an endless scarf. You get exactly as much fabric as you knit, and the pattern costs the same whether you knit five rows or none.

saying these in an interview costs you the question

  • Thinks the runtime produces all the elements up front and just stops early.
  • Says an endless sequence needs a large but finite cap to be safe.
  • Believes the producer, not the consumer, decides how many elements exist.
  • Claims the definition costs memory proportional to the number of elements.
  • Confuses the definition being cheap with every consumer of it being cheap.
open as a page

In a short-circuiting boolean AND, what does the left operand let you guard against?

level: juniorimportance: must knowfreq 74%

basics

~20 s

A short-circuiting AND evaluates its right operand only when the left one is true, so the left test can establish the precondition the right test needs - presence, a non-empty collection, a valid index - instead of hoping it holds.

open as a page

Under call-by-value, when is an expensive diagnostic argument evaluated if the check that receives it usually ignores it?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Call-by-value evaluates every argument at the call site, before the check's body starts. The expensive diagnostic is built on every call, including the calls where the check discards it unused.

open as a page

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%

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.

open as a page

An eager three-stage chain over a large export versus the same stages run lazily - what differs in traversals and memory?

level: middleimportance: must knowfreq 62%

basics

~20 s

The eager chain walks the data once per stage and builds a complete collection at every stage boundary. The fused lazy chain walks it once and holds one element in flight, so no collection exists at a boundary - only the final result is built.

open as a page

Which steps over an endless back-off delay sequence never return, and what separates them from ones that do?

level: middleimportance: must knowfreq 55%

basics

~20 s

Any step whose answer depends on the last element - a count, a total, a maximum, a sort, a copy into a collection - never returns over an endless producer. Steps that can commit after a finite prefix do return.

open as a page

A scan that stops at the first duplicate it finds - what does that early exit actually save?

level: middleimportance: must knowfreq 58%

basics

~20 s

Early exit saves everything after the answer: the elements never read and the per-element work never done. It cuts the expected cost, often enormously, but leaves the worst case untouched - an input with no duplicate is still read to the end.

open as a page

Call-by-name and call-by-need both defer an argument until it is used, so what does call-by-need add?

level: middleimportance: must knowfreq 54%

basics

~10 s

Call-by-need adds caching. Both defer the argument until the body demands it, but call-by-name re-evaluates the expression at every use, while call-by-need evaluates it once on first demand and reuses the stored result.

open as a page

A running total over a long scan defers each addition instead of performing it. Why does memory grow with the element count?

level: middleimportance: must knowfreq 50%

basics

~20 s

Each deferred addition allocates a thunk that references the previous one, so the accumulator grows into a chain as long as the input instead of staying a single number. Nothing collapses until the final force walks the whole chain.

open as a page

A lazy chain of three stages over a stock-take export logs every row it sees - in what order do those log lines appear?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Interleaved per row, not grouped per stage: the first row passes through all three stages, then the second row does. A lazy chain pulls one element at a time through the whole chain instead of finishing one stage over the whole export.

open as a page

In a single-pass pipeline, why does putting a cheap selection before an expensive transformation reduce the work done?

level: middleimportance: should knowfreq 55%

basics

~20 s

Because each stage's function runs once per element that reaches it. Fusion removes the collections between stages, not the per-element work, so the only way to run the expensive transformation fewer times is to let fewer elements reach it.

open as a page

A back-off helper can cap its delay sequence at ten entries or hand back an endless one - what does each choice give the caller?

level: middleimportance: should knowfreq 45%

basics

~20 s

Capping inside the producer makes every consumer safe but fixes one policy for all of them. An endless producer leaves stopping to the consumer, who can bound by count, by predicate or by deadline - or forget to bound at all.

open as a page

A first-match search runs after an expensive transform stage - why does eager evaluation call that transform more often than lazy evaluation does?

level: middleimportance: should knowfreq 50%

basics

~20 s

Under eager evaluation the transform stage finishes over the whole input before the search ever begins, so it runs once per element. Under lazy evaluation the search pulls elements one at a time and stops at the match, so the transform runs only up to that element.

open as a page

Which evaluation strategy still fails when an argument the body never uses would error or never terminate?

level: middleimportance: should knowfreq 43%

basics

~20 s

Call-by-value fails. Strict evaluation reduces the argument before the body runs, so a failing or non-terminating expression takes the call down even though the body would never have used it. Both non-strict strategies return normally.

open as a page

A lazy pipeline over a stock-take export still peaks at full-dataset memory - which kind of stage explains that?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A barrier stage: one that cannot emit its first output until it has consumed every input element, such as ordering or grouping. It buffers the whole stream, splitting the chain into two fused segments with a materialised collection between them.

open as a page

A stage keeps only back-off delays above a ceiling the endless producer never reaches, and taking one element hangs - why?

level: seniorimportance: should knowfreq 36%

basics

~20 s

A bounded demand terminates only if the source can satisfy it. The discarding stage keeps pulling to find a match, the clamped producer yields values below the ceiling forever, and the request for one element is never answered.

open as a page

A team reorders the two operands of a short-circuiting AND to put the cheaper test first - when does that break?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Reordering is safe only when neither operand depends on the other, neither has an effect the program needs, and neither can fail or hang. Break any of those and the swap changes behaviour, not just cost - most often by evaluating a test whose precondition no longer holds.

open as a page

You make one parameter of a heavily-called check non-strict; what must you verify about the arguments callers already pass?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Verify that existing argument expressions tolerate being evaluated later, elsewhere, possibly never, and possibly more than once: their effects, their failure timing, when they sample mutable state, and any reliance on left-to-right argument order.

open as a page

A team made every computed field of a record lazy; memory and latency both rose though fewer fields are read. What are they paying for?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Every deferred field costs an object on the heap plus a check and a pointer hop on each read. Where the computation is cheaper than that overhead, deferral adds allocation, indirection and collector pressure while saving almost nothing.

open as a page

Forcing a thunk raises an error instead of returning a value. What can the next demand do, and what does each choice cost?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Either the failure is cached, so every later demand raises the same error without re-running the body, or the cell stays unforced and the next demand retries. Caching keeps the value deterministic and run-once; retrying repeats the body and its cost.

open as a page