skip to content

A routine expects a roster of vehicles and you hold a fleet log of electric vans - which two relations must hold for the call?

level: seniorimportance: nice to knowfreq 26%

answer

  1. two things differ, not one
  2. separate the outer type from the argument
  3. ordinary subtyping, then the variance step
  4. a fixed argument still needs the step
  5. either step alone can reject

basics

~20 s

Two independent ones: the fleet log type must be related to the roster type by ordinary subtyping, and the element substitution from van to vehicle must be permitted by the roster's variance. Either one missing rejects the call.

solid answer

~40 s

When both the container type and the element argument differ, the substitutability question splits in two. First, is a fleet log a roster at all? That is plain subtyping between the two parameterized types and has nothing to do with variance. Second, may a roster over electric vans stand in for a roster over vehicles? That is the variance step. The call type-checks only when both hold, and they fail independently: a fleet log that is not a roster is refused however permissive the element position is, and a fleet log that is a roster of electric vans is still refused if the element position is invariant. Candidates who blur the two describe the whole thing as "variance" and then cannot say why an unrelated log type is refused.

code

pseudocode · 8 lines
pseudocode
type VanLog is a Roster<ElectricVan>   // outer relation; element argument fixed here

function auditAll(fleet: Roster<Vehicle>)

log: VanLog = loadVanLog()
auditAll(log)
// step 1: VanLog is a Roster<ElectricVan>          -> holds by declaration
// step 2: Roster<ElectricVan> for Roster<Vehicle>  -> only if the element position permits it

go deeper

for a junior

Notice when two things differ at once - the container type and its element argument - and that each has to be justified separately.

for a middle

Explain the two steps in order and give the case where each one alone refuses the call.

for a senior

Diagnose in the right order: establish the outer relation first, and only then argue about the element position.

for a principal

Treat the argument a shared type declares itself at as a published decision, since it determines which expectations that type satisfies without any variance step.

## Two axes, not one Every earlier form of this question varied one thing: the element argument, with the container type held fixed. Real code varies both. A routine expects `Roster<Vehicle>`; you are holding a `FleetLog<ElectricVan>`, or a `VanLog` that is declared to be a `Roster<ElectricVan>`. Now two independent questions must both be answered yes: 1. **The constructor step.** Is the outer type related to the expected outer type by ordinary subtyping? Is a fleet log a roster? This is the same subtyping any non-parameterized pair of types goes through, and **variance plays no part in it**. 2. **The argument step.** Given that relation, may the element argument change from electric van to vehicle? This is the substitutability question, and its answer is the container's variance in that position. The call is accepted only if both hold. They fail independently, which is exactly what makes this worth asking. ## Working the cases | What you hold | Accepted where `Roster<Vehicle>` is expected? | What it needs | |---|---|---| | `Roster<Vehicle>` | yes | nothing - a type stands in for itself | | `VehicleLog`, declared to be a `Roster<Vehicle>` | yes | the constructor step only | | `Roster<ElectricVan>` | only if the element position permits it | the argument step only | | `VanLog`, declared to be a `Roster<ElectricVan>` | only if the element position permits it | both steps | | `FleetLog<ElectricVan>`, unrelated to `Roster` | no | nothing bridges it | The fourth row is the interesting one and the one candidates get wrong. `VanLog` has **fixed** its element argument at the point it declared itself a roster - it is a roster of electric vans, full stop, with no type parameter left to vary. That fixing is not a variance step; it is an ordinary declaration. The van-to-vehicle move is still ahead of you, and it still needs the element position to permit it. The last row is the corrective one. If the fleet log type is simply not a roster, no amount of permissiveness in the element position helps, because there is no constructor relation for the argument step to apply to. An engineer who has learned "variance decides whether containers substitute" and nothing else cannot explain that refusal. ## The order matters when you diagnose When a call like this is refused, check the constructor step first. It is cheap to establish - either the log type declares itself a roster or it does not - and if it fails, nothing about variance is relevant to the diagnosis. Only once the outer relation is established does the element argument become the question. Diagnosing in the other order produces the familiar dead end of arguing about directions for a type that was never a roster to begin with. ## A type may reach a container at a chosen argument One refinement worth knowing: a type that declares itself to be some parameterized container gets to choose the argument it does so at. A log type may declare itself a roster of electric vans, or a roster of vehicles, or - if it carries its own parameter - a roster of whatever that parameter is. Choosing the broader argument at the declaration is an alternative to needing the variance step later, and it is a design lever: the type's author decides once, at the declaration, which expectation their type satisfies directly. Whether a single type may declare itself the same parameterized container twice at two different arguments is one of the places type systems genuinely differ, so it is not something to assume. ## What the interviewer is checking This is a decomposition question. The strong answer names the two steps, says that variance belongs only to the second, and gives the case that isolates each: an unrelated log type fails the first step whatever the variance; a log that is a roster of vans fails the second step under invariance. The weak answer treats "can I pass this?" as one undifferentiated variance question, which works for the simple case and collapses the moment the outer type changes too. ## How to say it in one breath "Two things have to line up. Is the log a roster - plain subtyping, nothing to do with variance. And may a roster of electric vans stand in for a roster of vehicles - that is the variance step. Fixing the element argument at the declaration settles the first, not the second."

  • A log type declares itself a roster of vehicles rather than a roster of electric vans. What changes?
    The variance step disappears. The element argument already matches what the routine expects, so ordinary subtyping carries the call and the element position's answer is irrelevant. Choosing the broader argument at the declaration is a design lever the type's author holds.
  • The log type is unrelated to the roster type. Does a permissive element position help?
    No. Without a constructor relation there is nothing for the argument step to apply to, so the call is refused however the element position is answered. Diagnose the outer relation first, or you will argue about directions for a type that was never a roster.

saying these in an interview costs you the question

  • Treating the whole call as one undifferentiated variance question
  • Believing that fixing the element argument at a declaration removes the variance step
  • Expecting a permissive element position to relate two unrelated container types
  • Claiming the outer relation follows from the element relation
  • Assuming any type may declare itself the same container at two different arguments