skip to content

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%

answer

  1. scheduling, not the computed result
  2. who asks whom for a value
  3. element-at-a-time, not stage-at-a-time
  4. one row visits every stage first
  5. log interleaves by row, not stage

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.

solid answer

~40 s

The lines come out grouped by row, not by stage. A lazy chain is driven from its end: the collecting step asks the last stage for one value, that stage asks the stage before it, and so on back to the source, so a single row travels the whole chain before the next row is read. With rows `A` and `B` the log reads parse `A`, keep `A`, format `A`, parse `B`, keep `B`, format `B`. An eager chain would print all the parse lines first, then all the keep lines, then all the format lines, because each stage finishes over the entire export before the next one starts. The collected result is identical either way - only the scheduling, and therefore the interleaving, differs.

code

pseudocode · 12 lines
pseudocode
stage1 = function(row)   log("parse " + row.id);   return parse(row)
stage2 = function(item)  log("keep " + item.id);   return item.counted > 0
stage3 = function(item)  log("format " + item.id); return format(item)

source = lazy(rows)          # nothing read yet
p1 = map(source, stage1)     # still nothing read
p2 = filter(p1, stage2)      # still nothing read
p3 = map(p2, stage3)
result = collect(p3)         # the export is walked here, once

# for rows A and B, the log reads:
#   parse A   keep A   format A   parse B   keep B   format B

go deeper

for a junior

Recall the shape of the answer: the lines interleave per row, because a lazy chain pulls one element at a time through all its stages. Being able to predict that log by hand is the whole question at this level.

for a middle

Explain the mechanism that produces the order - demand travelling backwards from the collecting step to the source, and one value travelling forward - and contrast it with the eager schedule, where each stage runs to completion over its entire input.

for a senior

Point out that the log is itself an observable effect, so a stage that logs is not pure, and that this interleaving is exactly what makes a debug print inside a stage safe to reason about only when you know which schedule you are on.

for a principal

The angle worth owning is whether your codebase makes the schedule visible at the call site at all. A chain whose laziness is implicit reads identically to an eager one and behaves nothing like it under load.

A staged pipeline - parse each exported row, keep the ones with a non-zero counted quantity, format what survives - says *what* three transformations compute. How those three stages are **scheduled** is a separate decision, and a log statement placed inside each stage is the cheapest way to see which schedule you actually got. ## Stage-at-a-time versus element-at-a-time **Stage-at-a-time** is the eager schedule. Each stage consumes its entire input and produces a complete output before the next stage begins. The parse stage reads all one hundred thousand rows and hands on a collection of one hundred thousand parsed items; only then does the selection stage start. **Element-at-a-time** is what a lazy chain gives you, and it is what "the stages are fused" means: the chain is not three loops but one, with the three per-element functions applied inside it. Nothing is read when the chain is built. When the collecting step runs, it asks the final stage for a value; that stage asks its upstream for a value; the request travels back to the source, one row comes forward, and it is pushed through every stage that is willing to accept it before the source is asked again. | What you observe | Stage-at-a-time chain | Element-at-a-time chain | |---|---|---| | Log order | every parse line, then every keep line, then every format line | one row's parse, keep and format lines, then the next row's | | First format line | after the export has been walked twice already | after the first surviving row | | Held between stages | a complete collection at each boundary | one element in flight | | Walks of the source | one per stage | one for the whole chain | ## Why demand decides the order The interleaving is not a convention someone chose; it falls out of who asks whom. 1. The collecting step demands the next value from the last stage. 2. The last stage has nothing buffered, so it demands a value from the stage before it. 3. That demand reaches the source, which yields exactly one row. 4. The row travels forward through the stages that accept it, and the value that emerges is appended to the result. 5. The cycle repeats until the source has no more rows. That is why the log is ordered by row: step 4 completes for one row before step 1 runs again. ## What the interleaving does and does not tell you - **The result is unchanged.** Both schedules compute the same collection from the same inputs; you cannot tell them apart from the output alone. - **The line counts per stage still differ.** A row dropped by the selection stage produces a parse line and a keep line but no format line, under either schedule. - **A dropped row ends its own trip immediately.** In the element-at-a-time chain, the selection stage rejecting a row sends the demand straight back to the source rather than forward. - **Logging is itself an observable effect.** It is how you see the schedule, and it is also the reason a stage function that logs is not a pure function - which is why the interleaving surprises people who have only ever reasoned about the values. - **Some designs make the schedule explicit, others infer it.** Some collection libraries give you a separate lazy view that you opt into, and others make every chain lazy until a collecting step runs; the log order tells you which one you are holding without reading any documentation. ## Why an interviewer asks it It is a "what does this print" question with no trick in it, and it separates a candidate who has a model of demand-driven evaluation from one who reads a chain of stages as three sequential loops. It also sets up everything that follows: once you can see that one row visits every stage before the next row is read, the absence of a collection at each stage boundary, and the effect of stage order on how often an expensive function runs, are both immediate consequences rather than facts to memorise.

  • What order would the same three log statements appear in if the chain were eager instead?
    Grouped by stage: every parse line for the whole export, then every keep line for the parsed items, then every format line for the survivors. Each stage runs to completion over its entire input before the next one starts, so a row's three lines are separated by the whole rest of the export.
  • One row is rejected by the middle stage. Which log lines does that row produce in the lazy chain?
    Its parse line and its keep line, and nothing after that. The rejection ends that row's trip through the chain immediately, so the final stage never sees it and prints nothing for it. The demand goes straight back to the source for the next row.

A passport desk processes one traveller through check, stamp and file before calling the next, rather than checking the whole queue, then stamping the whole queue.

saying these in an interview costs you the question

  • Thinks each stage finishes the whole export before the next stage starts
  • Says the interleaving changes the collected result, not just the timing
  • Believes the source is read when the chain is built rather than when it is collected
  • Expects a rejected row to still reach the final stage
  • Assumes every stage prints the same number of lines