skip to content

The build rejects your roster of electric vans where a roster of vehicles is expected - how do you read that?

level: seniorimportance: should knowfreq 40%

answer

  1. the checker is reporting, not failing
  2. the element relation is untouched
  3. a result about the container type
  4. a fresh container is always sound
  5. a cast silences, it does not relate

basics

~20 s

Read it as a variance result, not a tooling defect: that roster type is invariant in its element position, so the subtype relation between the elements does not carry to the containers. The element relation itself is untouched.

solid answer

~40 s

The rejection is the type system answering the substitutability question, and the answer is "no relation in that position". Nothing is broken and nothing has been forgotten: an electric van is still a vehicle, and a single van still passes anywhere a vehicle is expected. What was refused is the *container* substitution. From there the responses are: build a new roster over the expected element type and pass that, which is always sound but costs a pass and drops aliasing; narrow the routine's parameter if it genuinely only ever handles vans; or have the roster's author state a looser direction, if the container supports one. What you must not do is silence it - an unchecked cast or dropping the element argument removes the message without changing the fact it reported.

code

pseudocode · 10 lines
pseudocode
function auditAll(fleet: Roster<Vehicle>)

vans: Roster<ElectricVan> = loadVanRoster()

auditAll(vans)            // rejected: no relation in that element position

copy: Roster<Vehicle> = newRoster()
for each v in vans
    copy.add(v)           // sound: every electric van is a vehicle
auditAll(copy)            // accepted - one pass, and copy no longer tracks vans

go deeper

for a junior

Learn to read a refused container argument as a statement about two container types, not as evidence that the element relation is missing.

for a middle

Explain which substitution was refused and why copying into a container over the expected element type is sound in that direction.

for a senior

Demonstrate the judgment: weigh the copy's pass and its lost aliasing against narrowing the routine or changing what the container publishes.

for a principal

Set the norm that a refused substitution is a design signal reviewed at the type's boundary, and that silencing casts do not pass review.

## What the message is actually saying A routine is declared `auditAll(fleet: Roster<Vehicle>)`. You call it with a `Roster<ElectricVan>` and the build refuses. The instinct of an inexperienced engineer is that the checker has lost the plot - vans are obviously vehicles, it says so two lines up. The instinct is wrong, and recognising why is the point of this question. The refusal is a **result**, not a failure. The type system asked the substitutability question about `Roster` in that element position and got the answer "no relation either way" - the invariant answer. Three things follow immediately: - **The element relation is intact.** `ElectricVan` is still a subtype of `Vehicle`; a single van still passes to anything taking a vehicle. - **The refusal is about two container types**, `Roster<ElectricVan>` and `Roster<Vehicle>`, neither of which is a subtype of the other. - **It is specific to that parameter position** of that container - not a general statement about the program. ## The three sound responses 1. **Build a container over the expected element type.** Create an empty `Roster<Vehicle>`, put the vans into it, and pass that. Sound in every case, because each van is a vehicle. It costs one pass over n elements, and - the part people forget - **the two rosters no longer alias**: later additions to the original are invisible to the copy and vice versa. For a large or open-ended source that cost is the whole decision. 2. **Narrow the routine.** If the routine only ever handles vans, declare it over the narrower element type. This is the right answer surprisingly often: the parameter was written broadly out of habit rather than need. 3. **Change what the container publishes.** If you own the roster type and it can support a looser direction, state one. That decision belongs to the container's author and is derived from what the container offers, which is a separate subject - the relevant point here is that it is a deliberate change to a published type, not a local fix at your call site. ## The responses that only look like fixes | Move | What it changes | What it does not change | |---|---|---| | An unchecked cast at the call site | The message disappears | The relation the message reported | | Dropping the element argument entirely | Checking is skipped for that value | Whatever the routine then does to the container | | Re-declaring a local variable at the expected type | Nothing - the same assignment is refused | The rejection simply moves one line up | All three are recognisable in review as an engineer arguing with the checker rather than reading it. The first two trade a compile-time message for a run-time outcome; the third does not even do that. ## Diagnosing which response is right Ask three questions in order: 1. **Does the routine genuinely need the broader element type?** If not, narrow it - cheapest fix, no copy, no aliasing change. 2. **Does the caller need the routine to see later changes to its roster?** If yes, a copy is not a fix at all, and the answer must come from the container's declared direction or from restructuring the call. 3. **How large, or how open-ended, is the source?** A copy over a bounded collection is usually nothing. A copy over an unbounded or expensively-produced source is a design change wearing a one-line disguise. ## Why interviewers ask this one It separates candidates who treat the type system as an obstacle from candidates who treat it as an instrument. The first group reaches for the cast and calls it a workaround; the second reads the rejection as information - *this container makes no promise about that substitution* - decides whether the promise should exist, and picks the response that matches the constraint the caller actually has. The senior signal is the aliasing point: noticing that the copy is not a free equivalent of the call you wanted, because you now hold two containers that will drift apart. ## The shape of a good answer "The rejection is telling me the roster type is invariant in that position, so the van-to-vehicle relation does not lift to the containers. The element relation is fine. My options are to copy into a roster of vehicles - one pass, and the two stop aliasing - to narrow the routine if it only ever deals with vans, or to get the container's published direction changed if it can support one. What I would not do is cast it away, because that hides the result without changing it."

  • What exactly does the caller give up by copying into a roster of the expected element type?
    A pass over n elements, and aliasing. The copy is a separate container: additions or removals on the original after the call are invisible through it, and vice versa. Where the routine was meant to observe a live roster, the copy is not an equivalent call.
  • The team's fix is an unchecked cast at the call site. What is wrong with that argument?
    The cast removes the message, not the relation it reported. The container's promises are unchanged, so whatever the routine does through the expected type is now unchecked; any resulting failure surfaces away from the line that caused it.

saying these in an interview costs you the question

  • Saying the compiler lost track of the subtype relation between the elements
  • Concluding electric vans are not really a subtype of vehicles here
  • Calling the rejection a known limitation cured by a cast
  • Treating the copy as a free equivalent of the call you wanted
  • Re-declaring a local variable at the expected type and expecting it to pass
  • Generalising the result to every parameterized type in the program