skip to content

Why is a routing registry that maps route keys to handler payloads declared with two placeholders rather than one?

level: juniorimportance: must knowfreq 60%

answer

  1. count the knobs, not the positions
  2. one argument per placeholder per use
  3. repeating a placeholder forces agreement
  4. key and payload vary apart
  5. lookup returns the chosen payload

basics

~20 s

A key type and a payload type vary independently, so each needs its own placeholder. A placeholder takes one type argument per use, so a single placeholder would force every route key and its payload to be the same type.

solid answer

~50 s

A placeholder is filled by exactly one type argument at each use site, so the number of placeholders is the number of things a caller may choose independently. A routing registry has two: the type that identifies a route, and the type of payload its handler receives. Declaring `Registry<Key, Payload>` lets one module pair identifier keys with order payloads while another pairs text keys with command payloads, each combination checked as a pair. Collapsing them into `Registry<Entry>` does not merely read worse — it quietly requires key and payload to be the same type at every insertion, which no routing table wants. The second placeholder also makes the read side precise: a lookup is declared to return `Payload`, so on a `Registry<RouteId, OrderPayload>` the caller gets an order payload back rather than something it has to identify.

code

pseudocode · 12 lines
pseudocode
type Registry<Entry>:
    entries: list of pair(Entry, Entry)

type Registry<Key, Payload>:
    entries: list of pair(Key, Payload)

function put(reg: Registry<Key, Payload>, k: Key, p: Payload)
function lookup(reg: Registry<Key, Payload>, k: Key) -> Payload or none

// one knob: whatever fills Entry types BOTH halves of every pair
// two knobs: RouteId keys with OrderPayload values, checked as a pair
// lookup on Registry<RouteId, OrderPayload> returns an OrderPayload

go deeper

for a junior

Remember the counting rule: one placeholder equals one type a caller may choose, used everywhere that placeholder appears. Two independent things to vary means two placeholders.

for a middle

Explain the mechanics: the argument is bound once per use site and reused at every mention, which is why repeating a placeholder is a constraint rather than a second knob, and why the read side can promise the exact payload type.

for a senior

Show you read a signature the way a caller does. Say what each placeholder lets a call site vary, catch the accidental constraint of a reused placeholder in review, and flag a positional swap as a breaking edit for explicit callers.

for a principal

Frame placeholder count as an interface commitment. Every placeholder you publish is a choice callers may make forever, and every one you leave out is a constraint you have imposed on them without saying so.

## A placeholder is one choice, made once per use A **type parameter** — a placeholder — is a name written into a declaration to stand for a type that the declaration deliberately refuses to fix. At every use site the placeholder is filled by exactly **one** type argument, and that argument is then used for every position in the declaration that mentions it. One consequence follows, and it decides the whole question: **the number of placeholders in a declaration is the number of things a caller may choose independently.** - One placeholder means one free choice per use. - Two placeholders mean two free choices, made without reference to each other. - A placeholder repeated in several positions is **not** a second choice. It is the same choice appearing twice, and that repetition is precisely what forces those positions to agree. The third bullet is the one candidates miss, and it is exactly where a single-placeholder registry goes wrong. ## The routing registry with a single placeholder Take a routing table: values of some type identify a route, and each route is stored with the payload that its handler will be given. Written with one placeholder, the stored pair becomes a pair of the *same* type: ``` type Registry<Entry>: entries: list of pair(Entry, Entry) ``` This declaration is perfectly legal, and that is the trap — nothing rejects it. What it says is that whichever type a caller supplies for `Entry` is used for **both** halves of every pair. A caller who wants short identifier route keys and structured order payloads has to find one type that covers both, and then both halves are typed as that one thing: the key position no longer promises a key and the payload position no longer promises a payload. The rule "a route key and its payload are the same type" was never something a routing table asked for. It arrived because the declaration offered only one knob. ## The same registry with two ``` type Registry<Key, Payload>: entries: list of pair(Key, Payload) ``` Now each use site makes two decisions, and they are unrelated to each other: - one module builds a `Registry<RouteId, OrderPayload>`; - another builds a `Registry<Text, Command>` from the same declaration; - neither type argument has to stand in any relationship to the other, and no shared supertype has to exist for the pair to make sense. ## What the second placeholder buys on the way out The insert side is the obvious half: the checker rejects a payload written into the key position. The read side is where the second placeholder earns its keep. 1. A lookup is declared to return `Payload`, so on a `Registry<RouteId, OrderPayload>` it returns an order payload and the caller can use it directly. 2. Iteration hands back pairs whose halves are distinguishable, so a walk over the table can match on one half and act on the other. 3. A helper that transforms only the payload side can be written against `Payload` alone, leaving `Key` untouched and unmentioned. | Declaration | What a caller may vary | What a lookup can promise | Fits when | |---|---|---|---| | `Registry<Entry>` | one type, used for both halves | an `Entry` — key and payload indistinguishable | the two halves genuinely are one type | | `Registry<Key, Payload>` | the key type and the payload type, apart | exactly the chosen `Payload` | they vary independently — the usual case | | a separate registry per key type | nothing; each declaration fixes both | its own fixed payload type | there are only ever one or two combinations | ## Reading a signature that carries several placeholders Two placeholders introduce something one placeholder never had: an **order**. - Type arguments bind to placeholders **by position**, not by name. `Registry<Key, Payload>` and `Registry<Payload, Key>` describe the same shape but swap what every call site has to write. - Where the caller writes the arguments explicitly and the two chosen types happen to be compatible, a swapped pair can be accepted and misbehave later; where the checker solves the arguments from the values passed in, the swap is usually caught at the call. - Once there are two, single-letter names stop paying for themselves. A reader matching the second or third single letter in a signature back to its meaning is doing work that a role name — `Key`, `Payload` — would have done for free. How far each ecosystem takes that convention differs; the reader's problem does not. ## Two is not automatically the right number Two placeholders are right when the two choices really are independent, and that is not the end of the story: - If the payload a route carries is decided **by the route itself**, the second placeholder is not free; the pair is dependent, and the caller only appears to make two choices. - If a declaration grows a third and a fourth placeholder that different call sites care about separately, the count has stopped describing one coherent thing. The question to ask of every placeholder is the same one each time: *what does a caller get to choose here that it could not choose otherwise?*

  • If the same placeholder appears three times in one declaration, how many choices does a caller make?
    One. A repeated placeholder is a single choice reused, and the repetition is the point: it is what makes those three positions agree. Degrees of freedom are counted by distinct placeholder names, not by how often each is mentioned.
  • Does swapping the declaration order of two placeholders change what the type can express?
    No — the set of expressible pairs is identical. It changes what every explicit call site has to write, because arguments bind by position rather than by name. A swap is therefore a breaking edit for callers that spell their arguments out, and invisible to callers whose arguments are solved from the values.
  • When is a single placeholder for both halves actually right?
    When the two halves genuinely are the same type by intent, not by accident — a pairing of a value with another value of its own type, a swap that returns the same pair reversed, or a table that maps a type onto itself. There the repetition is the constraint you want stated.

saying these in an interview costs you the question

  • Says two placeholders instead of one is only a naming or style choice
  • Thinks one placeholder can be filled by a different type at each position in the same use
  • Believes type arguments bind to placeholders by name rather than by position
  • Assumes a lookup on a two-placeholder registry can only promise a common supertype
  • Claims more placeholders always means a better and more flexible design