skip to content

Per-Type Implementations

Supplying a different implementation for chosen type arguments, fully or for a whole family, and how the most specific one is picked. Interviewers ask what happens when the behaviour then diverges.

on this pageshow

questions

4

An archiver's generic record writer also ships a hand-written body for one chosen type argument - what decides which body runs?

level: middleimportance: must knowfreq 58%

answer

  1. one general body, several narrower ones
  2. the pattern, not the value
  3. candidates gathered where the argument is fixed
  4. narrowest claim wins
  5. general body is everyone's fallback

basics

~20 s

The most specific matching definition wins. Where the type argument becomes known, every candidate whose pattern that argument satisfies is gathered, and the narrowest is selected; the general body is the fallback for arguments nothing narrower claims.

solid answer

~50 s

Specialization means the generic definition is no longer alone: there is a general body plus one or more narrower bodies, each labelled with the type arguments it claims - one exact argument, or a shape that a whole family of arguments satisfies. Selection is a static ordering by specificity, not dispatch on a value. Where a call fixes the type argument, the compiler gathers every candidate the argument matches, orders them by how much each claims, and picks the one whose claim is a strict subset of the others'. The general body matches everything, so it loses to any narrower candidate that matches and is visible at that point. Nothing consults the value at run time: two call sites holding the very same object can select different bodies if one has fixed the argument and the other still holds it generically.

code

pseudocode · 17 lines
pseudocode
function writeRecord<T>(out, record)              // general body
    for each field in fieldsOf(record)
        out.writeTagged(field)
    out.writeChecksum()

specialize writeRecord for <BlockRecord>(out, record)
    out.writeRaw(record.bytes)                    // fixed width, no tags
    out.writeChecksum()

specialize writeRecord for <Batch<T>>(out, record) // a whole family
    out.writeCount(count of record.items)
    for each item in record.items
        writeRecord(out, item)                    // re-selects per element

writeRecord(out, aBlockRecord)  // exact override: narrowest claim
writeRecord(out, aBatchOfRows)  // family override: narrower than general
writeRecord(out, aTextRecord)   // general body: nothing narrower claims it

go deeper

for a junior

Recall the shape: one generic routine can have extra hand-written bodies attached to particular type arguments, and the call looks identical either way. Knowing that the choice is made by the compiler, not by the caller, is enough here.

for a middle

Explain the mechanics: candidates are gathered where the type argument is fixed, ordered by whose claim is a strict subset of whose, and the unique narrowest is selected. Say plainly that declaration order and the runtime value play no part.

for a senior

Show that you track the selection point, not the value. In a real incident the useful question is what the calling context statically knew about the type, and whether the narrower body was even visible there.

for a principal

The tradeoff you own is whether a narrower body is allowed to exist in code other teams depend on, since selection is decided per call site and turns your library's behaviour into a property of each caller's static context.

## Two kinds of body under one name A **generic** routine is written once against a placeholder type it does not name. **Specialization** puts a second body under that same name: a hand-written implementation labelled with the type arguments it claims. There are two shapes of claim: - a **full override** fixes every type parameter - one exact argument, one body; - a **family override** (often called a *partial* specialization) fixes only part of the shape - "any batch, whatever its element type" - so it claims a whole family of arguments, including arguments its author never saw. Both are called the same way. A caller writes one call; nothing in the call text says which body it wants, and usually the caller does not know there is a choice. ## The candidate set Where a call fixes the type argument, three steps run: 1. **Gather.** Collect every definition under that name whose pattern the argument satisfies - the general body always qualifies, plus any family override whose shape the argument fits, plus a full override for that exact argument if one exists. 2. **Order.** Compare the candidates by how much each claims. 3. **Pick the unique narrowest.** If one candidate's claim sits strictly inside every other's, it is selected. If no single candidate is narrowest, the call is rejected as ambiguous. ## The most-specific rule Candidate **A is more specific than B** when every argument A matches is also matched by B, and B matches at least one argument A does not. A's claim is a **strict subset** of B's. For an archiver's record writer with three definitions: | Candidate | Arguments it claims | Position in the order | |---|---|---| | general body, any record type | every argument | widest; always present, wins only by default | | family body, any batch of any element | every batch, whatever the element | narrower than the general body | | full override, one fixed-width record type | exactly one argument | narrowest of the three | Things that follow from the rule, and that candidates routinely get wrong: - The general body is the **floor**, not the first attempt. It is not tried and then abandoned; it is simply the widest claim, so anything narrower outranks it. - **Declaration order is not the rule.** Specificity is computed from the patterns themselves. - A family override needs no list of members - it matches by **shape**, so a type nobody had written when the override was authored still selects it. - Two family overrides can be **incomparable**: each claims arguments the other does not, and then neither is narrowest. ## Selection follows knowledge, not values The decisive point is *where the type argument is fixed*, and that is a static property of the calling context: - The value is never inspected. Selection is complete before anything runs. - A call that names the concrete argument selects against that argument. A call inside another generic routine that has not fixed its own placeholder selects against whatever that context knows - which may be nothing narrower than the general body. - Languages differ on **visibility**: some require the narrower body to be in scope where the argument is fixed, so an override introduced later in the program is simply not a candidate there; some report that as an error, some do not. That is why "which body ran?" is answered by reading the call site's static knowledge, not by logging the value. ## What a narrower body is obliged to do Only one thing is checked: it must be callable in place of the general body - same call shape, compatible result. **Its behaviour is not checked against the general body at all.** No tool compares the two implementations, and nothing requires the narrower one to produce the same observable output. That freedom is the entire point (a fixed-width record can be written in one pass instead of field by field) and it is also the standing hazard: once the two bodies disagree about anything a caller can observe, the program's behaviour depends on which one the call site selected. So the mental model is: **one name, several claims, static ordering by specificity, and no behavioural contract enforced between them.**

  • Can a family override be selected for a type argument that did not exist when the override was written?
    Yes. A family override claims a shape, not a list. If the new argument fits the shape - a batch of some element type nobody had defined yet - it matches, and it outranks the general body. That is what makes family overrides useful and what makes them risky: their reach grows on its own as new types appear.
  • Once a narrower body exists, what is the general body still for?
    Two things. It is the implementation for every argument no narrower body claims, and it is the de facto specification: it is the behaviour callers wrote their code against before any override appeared. Deleting it is not an option, and changing it silently changes the meaning of every call that still lands on it.
  • If a call site and a narrower body are in different parts of the program, can the call still select it?
    Only where the selection point can see it. Languages differ: some require the narrower body to be visible where the type argument is fixed and reject or ignore a later one, others accept a program where two call sites disagree. The safe rule is to declare every override with the general body, not near the type it is written for.

saying these in an interview costs you the question

  • Says the runtime inspects the value to choose the body
  • Thinks the general body runs first and hands off on failure
  • Believes declaration order decides which body wins
  • Assumes only one exact argument can carry a narrower body
  • Confuses it with choosing among same-named routines by argument type
open as a page

In your archiver, the hand-written body for one record type drifted from the general body - why does the bug appear only at some call sites?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Because which body runs is decided by what each call site statically knew about the type, not by the value. A site that fixed the argument takes the drifted body; a site still holding it generically can keep running the general one.

open as a page

Two specialized bodies of an archiver's writer both match one call and neither is narrower - what does the compiler do?

level: middleimportance: should knowfreq 40%

basics

~20 s

It rejects that call as ambiguous. Specificity is a partial order, so when each matching body claims arguments the other does not, no candidate is narrowest and there is nothing to select - the error lands at the call, not at either declaration.

open as a page

Other teams want to register specialized bodies for your shared archiver writer - what policy do you set?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Decide first whether specialization is part of the published contract or an internal optimisation. If outside teams may add bodies, publish the observable behaviour every body must reproduce, restrict overrides to cost and layout, and make a conformance suite the price of admission.

open as a page