How does a self-referential limit on a base builder stop a chained configuration step from degrading to the base type?
answer
- the base cannot name the derived type
- a placeholder meaning most-derived type
- every inherited step returns that placeholder
- leaf types close the loop on themselves
- a hook returning the receiver as the placeholder
basics
~20 sThe base declares a placeholder standing for the most-derived type, limits it to types deriving from the base at that placeholder, and returns the placeholder from every chained step - so each call hands back the derived type and later derived-only steps still resolve.
solid answer
~40 sThe problem is that a step declared on the base can only promise the base as its return type, so the moment a caller invokes an inherited step the chain drops to the base and every derived-only step after it stops resolving. The fix is to give the base a placeholder `S` that stands for "the most-derived type", limit `S` to types that derive from the base parameterized on `S`, and have every inherited step return `S`. Each derived type closes the loop by naming itself as the argument. A small hook that returns the receiver as `S` is what lets the base's shared step bodies produce that type, and it is the one piece each subtype has to supply.
code
pseudocode · 16 linestype EntrantBuilder<S where S derives from EntrantBuilder<S>>:
abstract function self() returns S # each subtype hands back its receiver
function named(name):
this.label = name
return self() # static type is S, not EntrantBuilder
type SeededBuilder derives from EntrantBuilder<SeededBuilder>:
function self() returns SeededBuilder: return this
function seed(n): this.seed = n; return this
b = new SeededBuilder()
b.named("north").seed(1) # resolves: named() handed back SeededBuilder
# had named() been declared to return EntrantBuilder,
# .seed(1) would not resolve at allgo deeper
Recall the symptom rather than the cure: a chained call that goes through an inherited step loses the specific type, so the next specific step will not compile. Knowing why that happens is enough at this stage.
Explain the mechanics end to end: a placeholder standing for the most-derived type, the limit that ties it to the base, every inherited step returning it, and the one hook each concrete type implements.
Show that you have maintained one. Name the intermediate-layer rule, the failure when a non-leaf closes the loop early, and the alternative of narrowing an override's return type with its cost.
Weigh whether a fluent surface justifies a parameter on every layer of a published base. Consider what it does to error messages and to teams who will extend the type without you in the room.
## The failure the shape exists to fix A base type carries steps that every entrant builder needs - naming, tagging, a display label - and derived builders add their own, such as assigning a seed number. A caller writes the steps in one chain. The moment the chain touches an inherited step, the static type of the expression becomes whatever that step declared as its return type, and a step declared on the base can only name the base. So the chain looks like this: 1. Call a derived step: the expression's type is still the derived builder. 2. Call an inherited step: the expression's type is now the base builder. 3. Call a derived step again: it does not resolve - the base has never heard of it. The usual workaround is to force the type back after every inherited step, which is exactly the run-time-risk-for-convenience trade that the type system was supposed to remove, and it has to be repeated at every call site in every caller's code. ## The shape that fixes it Give the base a placeholder - call it `S` - whose meaning is *the most-derived type in this chain*. Limit `S` so that only a type deriving from the base parameterized on `S` may fill it. Then declare every inherited step's return type as `S` rather than as the base. Each derived builder closes the loop by naming **itself** as the argument. From then on: - an inherited step called on the derived builder has static type `S`, which is the derived builder; - a derived-only step is therefore still in scope after it; - the order of steps in the chain no longer matters, which is the whole point of a fluent surface. ## The hook every subtype supplies The base's own step bodies have a receiver, but the receiver's static type inside the base is the base, not `S`. So the base cannot *produce* an `S` on its own. The standard resolution is a one-line hook: - the base declares an abstract operation returning `S`; - each concrete derived type implements it by returning its own receiver; - every shared step body ends by calling that hook and returning its result. That hook is the practical price of the shape. It is mechanical, it is easy to get wrong in exactly one way - an intermediate type that forgets to re-implement it - and it is what a reviewer should look for when reading such a hierarchy. ## Where the guarantee actually holds | Declaration | Return type of an inherited step | Chain after it | |---|---|---| | Step returns the base | The base | Derived-only steps no longer resolve | | Step returns the placeholder `S` | The most-derived type that closed the loop | Any step of that type resolves | | Step returns the receiver's own declared type, where the platform allows narrowing an override's return | The overriding type, per override | Works, but every level must restate every step | The third row is the alternative worth naming in an interview: some platforms let an override narrow its return type, which solves the chain without a placeholder - at the cost of restating each inherited step in each derived type. The placeholder solves it once for an arbitrarily deep hierarchy; the override approach scales with steps times levels. ## Intermediate layers The subtle part is a hierarchy three levels deep. An intermediate type that is itself meant to be extended must *stay generic* - it passes its own `S` upward to the base rather than closing the loop - and only the leaf types name themselves. An intermediate type that closes the loop early freezes the chain at that level, and everything below it suffers the original failure again. This is the most common defect in hand-written hierarchies of this shape and it is worth stating explicitly: 1. Leaf types close the loop by naming themselves. 2. Every non-leaf type stays parameterized and forwards its placeholder. 3. Only a type that closes the loop may be used bare, without an argument, by callers. ## The bill Every layer's declaration grows a parameter, the limit reads back on itself, and a caller who only wants to *store* one of these builders must decide what to write for the argument. Compiler messages produced from such a hierarchy tend to print the expanded limit rather than the type the author had in mind. That is the readability cost, and it is why this shape belongs on a base that is genuinely extended by other people, with the reason written down beside it, rather than on any type that happens to have a fluent surface.
- In a three-level hierarchy, which levels name themselves as the argument?Only the leaves. An intermediate type that is itself meant to be extended keeps its own placeholder and forwards it to the base; if it closes the loop by naming itself, the chain freezes at that level and everything below it loses derived-only steps again. That mistake is the usual defect in hand-written hierarchies of this shape.
- Why does the base need a hook that returns the receiver instead of just returning it directly?Inside the base, the receiver's static type is the base, and the base is not the placeholder. So a shared step body has no way to produce a value of the placeholder type on its own. The abstract hook pushes that one obligation down to each concrete type, which does know its own identity and can satisfy it in a single line.
- What alternative removes the placeholder entirely, and what does it cost?Where the platform lets an override narrow its return type, each derived type can re-declare the inherited steps with its own type as the result. The chain then works with no placeholder anywhere. The cost scales badly: the work is steps multiplied by levels, repeated by hand, whereas the placeholder solves it once for a hierarchy of any depth.
saying these in an interview costs you the question
- Thinks the step can simply be declared to return the derived type on the base
- Has every level, not just the leaves, name itself as the argument
- Believes forcing the type back after each step is equivalent and free
- Says the placeholder is resolved when the chain runs rather than when it compiles
- Assumes the base can return its own receiver as the placeholder without a hook