skip to content

A log loop computes the gap between each request and the previous one; why can no per-element stage do that directly?

level: middleimportance: nice to knowfreq 30%

answer

  1. a step that needs the step before
  2. stages are independent by construction
  3. make the neighbour into data
  4. n elements give n-1 pairs
  5. first element needs a decision

basics

~20 s

A per-element stage sees one element, while the loop had an index and could reach the element before it. A gap needs two elements, so pair each line with its predecessor first, or thread the previous value through the traversal explicitly.

solid answer

~50 s

The loop body is not really a per-element function: it reads the element at the current position *and* the one before it, so each step depends on the step before. A stage that is handed one line has no way to reach backwards, and no amount of rearranging stages invents the missing element. There are three honest routes. Pair each line with its predecessor first, turning the dependency into data so a plain per-element stage can subtract; carry the previous value along with the result in an explicit accumulator, which states the dependency instead of hiding it in a loop variable; or keep the loop, because a step that depends on the step before is precisely the case the loop form was designed for. All three do the same work - only the second and third leave the dependency visible.

code

pseudocode · 5 lines
pseudocode
function gaps(times)
    if length(times) < 2
        return empty list
    pairs = pair each element in times with the element before it, skipping the first
    return transform each pair in pairs to pair.current - pair.previous

go deeper

for a junior

Know that a stage is handed one element only, so comparing a line with the one before it needs the pairing to be built first.

for a middle

Name the dependency between consecutive steps, give both explicit forms - pairing neighbours or carrying the previous value - and say what happens to the first element.

for a senior

Judge when the rewrite pays: local neighbour relations usually do, several carried values usually do not, and a reviewer should hear which one you decided.

for a principal

The lead's angle is where the standard bends: a blanket "no loops" rule turns carried-state code into worse code, so the rule needs the exception written into it.

Most loops over an access log are per-element in disguise: each line is examined on its own and folded into a result. The gap calculation is not one of them, and it is the cleanest example of the thing that resists a straight rewrite. ## Why the stage cannot see the neighbour A transformation stage is a function applied to one element. It receives that element and nothing about the traversal - no index, no memory of what came before, no handle on what comes next. The loop reached the previous line by subtracting one from its position. Remove the position and that reach disappears with it. This is what makes the loop **carry a dependency between iterations**: step *k* needs a value that only exists because step *k-1* happened. A per-element stage is by construction independent of every other element, so the two shapes do not line up. The same shape shows up all over a log summary: a rolling average of the last few durations, detecting a burst by comparing consecutive arrival times, numbering requests within a session, marking each line as a repeat of the previous one. ## Three honest routes 1. **Make the neighbour into data.** Build a stage that pairs each line with the one before it, then the gap is a plain per-element function over pairs. The dependency has not gone away; it has been paid for once, up front, in one named step. 2. **Thread the state explicitly.** Carry both the result so far and the previous element through the traversal as one value, so each step is handed everything it needs. This is the most faithful translation - it says out loud what the loop variable was doing silently - and it is also the least readable of the three when the state has more than one part. 3. **Keep the loop.** A dependency between consecutive steps is the case the loop form exists for. Restating it as stages is legitimate, but it is not automatically clearer, and a reviewer is entitled to ask what the rewrite bought. | What the loop used the position for | The explicit form | |---|---| | read the element before this one | pair each element with its predecessor | | keep a value from the last iteration | carry it in the accumulator alongside the result | | know how far along we are | pair each element with its position | ## The boundary that comes back Pairing n elements with their predecessors yields n-1 pairs, and the first line has no predecessor. That is a decision, not an accident: drop the first line, or supply an explicit starting value to compare it against. Either is defensible and the two give different answers, so it belongs in the code where a reader can see it. It is worth being precise about how this relates to the usual claim that transformation chains remove off-by-one errors. They remove the *arithmetic* - a start, a bound and an advance that must all be right on every path through the body. What survives here is a single boundary **decision** about the first element, made once and visible. That is a much smaller surface than an index, but it is not zero, and claiming otherwise is the overstatement an interviewer will push on. ## Why this question separates candidates A candidate who has only ever rewritten independent loops will assert that anything can be expressed as stages and reach for one. A candidate who has done it in production recognises the dependency on sight, names it, and then chooses - usually pairing when the neighbour relation is simple and local, and usually the loop when several values have to be carried at once. The tell is whether they mention the first element without being prompted.

  • Does pairing neighbours also restore the ability to split the work across chunks?
    Mostly, and with one seam. Once the pairs exist, each pair is independent, so the per-pair work splits freely. Building the pairs is where the chunks touch: whichever component does the pairing has to join across a chunk boundary, or the pair that straddles it is lost.
  • When would you argue for keeping the loop here?
    When several values have to be carried from one step to the next, or when the decision at each step changes what happens later. The explicit accumulator then holds a small record of state and the stage body reads worse than the loop it replaced, with nothing gained beyond conformity to a style.

saying these in an interview costs you the question

  • Believes a per-element stage can look back at the previous element
  • Says such a loop cannot be expressed without mutation at all
  • Forgets that the first element has no predecessor to compare against
  • Treats pairing neighbours as free, ignoring the extra traversal
  • Claims the rewrite removes every off-by-one, including the boundary decision