skip to content

In a flat chain defining several entries in a row, what decides which entry a refinement step applies to?

level: seniorimportance: nice to knowfreq 28%

answer

  1. which entry is open?
  2. the receiver carries the target
  3. order becomes part of the contract
  4. narrow the return type per phase
  5. refinement with no target should not compile

basics

~20 s

The receiver decides. Each step returns an object carrying which entry is currently open, and a refinement attaches to that one. Nothing in the source text says so, which is why the chain's return types have to carry the target for the reader.

solid answer

~50 s

In a flat chain there is no visual nesting, so the only thing that knows which entry a refinement belongs to is the state carried by the receiver the previous step returned — the entry most recently opened. That makes order part of the contract even though nothing in the layout hints at it. Two designs follow. Under a single broad receiver type, every step is offered everywhere: a refinement written before any entry is opened type checks and then attaches to nothing, or worse to a default. Under narrowed return types, opening an entry returns a type that is the only one offering the refinement steps, so the ambiguity disappears — the reader can tell which target is live from the type, and an impossible order stops compiling. The price is one extra type per phase.

code

pseudocode · 9 lines
pseudocode
table
  .route("/orders")      // opens an entry -> OpenEntry
  .requireHeader("key")  // offered by OpenEntry -> /orders
  .route("/users")       // opens the next  -> OpenEntry
  .requireHeader("trace")// -> /users, not /orders
  .build()

// table.requireHeader("key")  <- not offered by Table:
// no entry is open, so there is no target to refine

go deeper

for a junior

Recall that a refinement in a chain attaches to the entry opened most recently, so moving one line changes what it configures even though the code still runs.

for a middle

Explain that the open target lives in the state the receiver carries, not in the layout, and that a single broad chain type therefore offers every step in every position.

for a senior

Show the silent failure — a refinement landing on the wrong entry — and design it out with a type per phase, while saying plainly which orderings that still permits.

for a principal

Judge whether the extra phase types are worth their vocabulary for this API's readers, and set when a chain should stop being flat at all.

## The ambiguity a flat chain creates A chain that defines one thing is unambiguous. A chain that defines several in a row is not. Read a routing table assembled as a single chain: a path is opened, a couple of refinements follow, another path is opened, more refinements follow. Every refinement is written at the same indentation as every other call, and nothing in the text says which path it belongs to. The answer is that **the receiver carries the target**. Each step returns an object whose accumulated state includes which entry is currently open, and a refinement applies to that entry. "Currently open" means *most recently opened by an earlier step in this chain* — a fact that lives in the values flowing through the chain, not in the source layout. Two consequences: - **Order is part of the contract.** Moving a refinement three lines up silently re-targets it. It is not a formatting change. - **The reader has no local signal.** Understanding one line requires scanning upward for the last opening step, which gets worse as the chain grows. ## Where this actually bites - A refinement written **before any entry is opened**: there is no target, so it either attaches to a default nobody intended or is dropped. - A refinement written **after the next entry was opened**: it lands on the new entry while the author was still thinking about the previous one. This one is invisible in review, because both lines are legal and both look right. - A refinement that is only meaningful **once another refinement has been made**, whose prerequisite was never written. All three share a shape: the chain type permitted a call whose meaning depended on state the type did not describe. ## Making the target visible in the return types | | One broad receiver type | A type per phase | |---|---|---| | What every step returns | the same chain type | a type naming the phase it leaves you in | | Calls offered after a step | all of them, always | only those legal in that phase | | A refinement before any entry | compiles, targets nothing | does not compile | | What the reader learns from a line | nothing about the target | which target is live | | Cost | one type, trivially extended | a type per phase, each named and documented | The technique is mechanical: 1. Give the opening step a return type that represents "an entry is open". 2. Offer the refinement steps **only** on that type. 3. Let each refinement return that same type, so refinements may repeat and combine freely. 4. Let the next opening step and the closing call be offered on it too, so the chain can move on or finish. Now the refinement cannot be written where there is no target, the code completion a reader relies on offers exactly the legal next calls, and the type at each point states which entry is live. Nothing outside the chain changed. ## What it does not fix Be precise about the boundary, because this is where a candidate oversells the technique: - It cannot stop a refinement being written **after the next entry was opened**. Both entries are in the same phase, so both orders type check and both mean something. Only a reader — or a rule that an entry may not be reopened — catches that. - It describes **structure, not values**. That two refinements conflict, or that a value is out of range, is checked when the closing call runs. - It multiplies types. A chain with many phases grows a vocabulary that has to be named well, or the cure reads worse than the disease. ## When to reach for it - **Worth it** when the chain routinely defines several things in one expression, when a mis-targeted refinement fails silently rather than loudly, and when the API is read far more often than it is changed. - **Not worth it** when the chain configures a single thing, when every step is independent so no target exists to confuse, or when the phases would change often enough that the extra types become a maintenance tax. The question an interviewer is really asking is whether you know that a flat chain's readability is partly an illusion: it reads like a sentence, but the thing each clause modifies is carried invisibly in the values flowing through it, and the return types are the only place a design can make it visible again.

  • Does narrowing the return types make the chain's step order fully safe?
    No. It stops a refinement being written where no target exists, but once two entries are both openable from the same phase, writing a refinement after the wrong opening step still type checks and still means something. That one is caught by reading, or by forbidding an entry to be reopened.
  • What is the readability cost of a type per phase?
    A vocabulary of intermediate types that exist only to carry position. Each needs a name a reader can make sense of, and each appears in errors and in signatures. On a chain with many phases the types can become harder to follow than the ambiguity they removed.

saying these in an interview costs you the question

  • Thinks a refinement applies to the whole chain at once
  • Assumes source proximity decides which entry is refined
  • Believes a flat chain makes step order irrelevant
  • Says narrowed return types also validate the accumulated values
  • Claims extra phase types cost nothing in API surface