skip to content

Variance

Whether a parameterized type over a subtype is itself a subtype, and what forces the answer: producer and consumer positions, annotation placement, and mutation. Interviewers use it to sort depth.

on this pageshow

questions

18

An array of premium seats is passed where an array of seats is expected, then a standard seat is stored - why is that unsafe?

level: juniorimportance: must knowfreq 62%

answer

  1. passing shares the buffer, does not copy
  2. who else still reads that array
  3. element type fixed when created
  4. reads keep the promise, writes break it
  5. damage at the store, symptom at the read

basics

~20 s

The array's real element type is still premium-only, so the store plants a value the original holder's type promises can never be there, and the next read of that slot returns something the reader believes is impossible.

solid answer

~40 s

Passing the array does not copy it: both sides now alias one buffer. Through the wider element type the routine is allowed to store any seat, but the buffer was created to hold premium seats and its original holder still reads it as premium-only. The moment a standard seat lands in a slot, that holder's type is lying about its own data, and the damage shows up later at a read that had nothing to do with the store. Reading through the wider type was always fine - every premium seat really is a seat. It is the write that breaks, which is why a container you can both read and write cannot be widened for free.

code

pseudocode · 8 lines
pseudocode
function fillEmpty(seats: Array<Seat>)
    seats[0] = makeStandardSeat()      // fine against this signature

block = newArray of PremiumSeat, size 100
fillEmpty(block)                       // allowed where arrays widen

p = block[0]                           // typed as PremiumSeat
p.loungeAccess()                       // but the slot holds a standard seat

go deeper

for a junior

Remember that passing a container shares it rather than copying it, and that the receiving code may store into it. Reading through a wider element type is safe; storing through one is not.

for a middle

Explain the mechanics: the buffer's element type is fixed at creation, the widened reference permits stores the buffer cannot honour, and the symptom appears at a later read by a third party.

for a senior

Show how you would find this in a running system - the exception or corruption lands far from the store, so you trace who else holds a reference to the buffer and which of them widened it.

for a principal

Frame it as an API rule for the codebase: mutable containers cross module boundaries at their own element type, and anything handed to readers goes out as a read-only view.

## The setting A stadium keeps its seating chart as an array. The general form is an array of `Seat`; one block of the stadium is held as an array of `PremiumSeat`, where every premium seat is a seat but not every seat is premium. Somewhere else there is a routine that takes an array of `Seat` and writes a standard seat into empty slots. Handing it the premium block feels obviously safe: premium seats are seats, so an array of them looks like an array of seats. It is safe for exactly half of what an array does. ## An array carries two promises, pointing opposite ways - **What may come out.** Every element is at least a `Seat`. Widening preserves this: read any slot of the premium block through the wider type and you get something that genuinely is a seat. - **What may go in.** Through the wider type, the type system says *any* `Seat` may be stored. Through the buffer's real identity, only a `PremiumSeat` belongs there. Widening keeps the first promise and destroys the second. That asymmetry - reads survive, writes do not - is the entire subject. ## The failure, step by step 1. The block is created as an array of `PremiumSeat` with room for a hundred entries. The element type is fixed at that moment and travels with the buffer. 2. The block is passed to a routine whose parameter is an array of `Seat`. No copy is made; the routine holds a second reference to the same buffer. 3. The routine stores a standard seat at index 0. Against its own signature this line is impeccable - it is storing a `Seat` into an array of `Seat`. 4. The original holder later reads index 0 through its own type, which says every slot holds a `PremiumSeat`, and asks the value for something only premium seats offer. 5. Step 4 is where the program misbehaves, and step 3 is where it was ruined. Nothing at step 4 is wrong. ## What each site believes | Site | What the static type says | What is actually true | |---|---|---| | The block's creator | every slot holds a premium seat | true until someone else stores | | The call that passes it | an array of premium seats is an array of seats | only for reading | | The store inside the routine | any seat may be written here | only a premium seat may | | The later read | this slot holds a premium seat | it holds a standard seat | ## Why the compiler is not in a position to catch it - The routine is **correct against its own signature**. Judged alone, it does nothing questionable, and it is typically compiled without ever seeing the call that will hand it the narrower array. - The two references **alias** one buffer. Proving that no writer anywhere can reach an array that some reader elsewhere treats as narrower is a whole-program question, not a local one. - The array can travel arbitrarily far - stored in a field, put inside another structure, handed on again - so the distance between the widening and the damaging store is unbounded. A language that permits this widening has therefore given up compile-time safety for that class of store, and must buy the safety back somewhere else or not at all. ## The general shape of the rule The seating chart is just the vivid instance. The rule is about **mutation**, not about arrays: - A container you may only **read** from can be used where a container of a wider element is expected - every element still satisfies the wider type. - A container you may **write** to cannot, because the write is typed against the wider element while the storage is still narrow. - A container that does **both** therefore gets the strict answer: it is usable only at its own element type. ## What a careful engineer does instead If the routine only reads, hand it something that only reads - a view exposing the reading side of the chart, which may safely widen. If it must write, it has to be typed against the element the buffer actually holds, or be made generic over that element so the caller's type argument travels with it. The one thing that is not an option is hoping nobody stores: the type system, not discipline, is what other people's code is checked against.

  • If the routine only read from the array and never stored into it, would the call still be unsound?
    No. Every element of the narrower array already satisfies the wider element type, so a read through the wider view can only hand back a value that genuinely fits. Unsoundness enters solely through the store. That is why handing out a read-only view of the same buffer is a safe way to grant access.
  • Does copying the array before passing it fix the problem?
    It removes the aliasing, so the original holder is no longer affected by stores into the copy - but the copy itself still has a narrow element type and can still be handed a value that does not fit. A copy fixes who gets hurt, not whether the store is type-correct.
  • Why is this widening more dangerous than an ordinary unchecked cast?
    An unchecked cast is written at one visible site by someone who knows they are overriding the compiler. This widening is implicit and looks like ordinary subtyping, so it happens in code nobody flagged, and the failure surfaces in a different routine at a different time.

Lending someone your box of first-class tickets after relabelling it "tickets": they are entitled to drop an economy ticket in, and you find out when you hand one to a first-class gate.

saying these in an interview costs you the question

  • Says it is fine because every premium seat is a seat
  • Assumes the array was copied when it was passed
  • Claims the compiler rejects the call, so nothing can happen
  • Blames the later read rather than the store
  • Thinks a cast at the read site would have prevented it
open as a page

Reading a row source declaration, what makes its placeholder a producer rather than a consumer?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A placeholder is a producer when every member that mentions it hands a value of it back: it appears only as output — a return type or a readable property — and never as a parameter the caller supplies.

open as a page

Electric vans are vehicles. Does it follow that a roster of electric vans is a roster of vehicles?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Not automatically. A subtype relation between two element types says nothing on its own about the containers over them; the container type decides. Three answers are possible - covariant, contravariant, invariant - and invariance is the usual default.

open as a page

A shared policy library can state a type's variance once on the declaration or let each caller narrow it - what does each buy?

level: middleimportance: must knowfreq 55%

basics

~20 s

Declaration-site states the direction once, so every use gets the subtyping free - but only for a type whose parameter sits in one direction. Use-site keeps the type invariant and lets each signature narrow its own view, repeatedly.

open as a page

In a language design where an array of a subtype is an array of its supertype, what must happen on every store?

level: middleimportance: must knowfreq 58%

basics

~20 s

Each store is tested at run time against the element type the array was actually created with, and a value that does not fit is rejected right there, at the write - not at the call that let the widened array through.

open as a page

A renderer reads rows from a source and writes lines to a sink — which one may take a more specific element type?

level: middleimportance: must knowfreq 68%

basics

~20 s

The source: because it only hands rows out, a source over a more specific row type is safe to pass. The sink only takes lines in, so it goes the other way and accepts one declared over a more general type.

open as a page

At a call site expecting a roster of vehicles, what does each of the three variance answers permit?

level: middleimportance: must knowfreq 58%

basics

~20 s

Covariance admits a roster of any vehicle subtype, contravariance admits a roster of any vehicle supertype, and invariance admits only a roster whose element argument is exactly the vehicle type. All three admit that exact roster.

open as a page

A signature narrows a read-write rule store to a producing view - which of its members stop being callable?

level: middleimportance: should knowfreq 44%

basics

~20 s

Every member that takes the element type as an input becomes uncallable on that occurrence: the narrowed view promises only production. Reads still return the declared element type, and the restriction applies to that occurrence alone, not to the type.

open as a page

A seating chart both reads out seats and accepts new ones - why is such a container safe to vary in neither direction?

level: middleimportance: should knowfreq 55%

basics

~20 s

Each direction breaks a different member: widening lets a caller store a plain seat into a premium chart, and narrowing lets a caller read a plain seat back as premium. With both members present, only the exact element type is sound.

open as a page

A type declared output-only in its placeholder adds a member taking that placeholder as a parameter — what happens?

level: middleimportance: should knowfreq 50%

basics

~20 s

The declaration itself is rejected, not any call site. A type that claims one direction must keep every mention of the placeholder on that side, and the checker reads each member signature to enforce it before any caller exists.

open as a page

Why is invariance the default answer for a parameterized roster type rather than an inferred direction?

level: middleimportance: should knowfreq 46%

basics

~20 s

Invariance is the answer that stays sound whatever the container offers, so it is the safe thing to assume when nothing has been said. It fails by rejecting a valid program, which is visible, rather than by admitting an unsound one.

open as a page

Consuming services keep hitting rejected arguments at a shared policy library's invariant signatures - where does the fix belong?

level: seniorimportance: should knowfreq 36%

basics

~20 s

With the library, not the services. The direction belongs to the signature or the declaration, and a caller cannot state it from outside. Narrowing each occurrence fixes one signature; declaring the direction fixes all of them, where the type allows it.

open as a page

A hot loop writes millions of elements into an array whose widening forces a per-store type check - what does that check cost, and when can a compiler drop it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

One extra type test per store: individually cheap and well predicted, but it sits on the write path and constrains bulk-store optimisations. A compiler may drop it only where the array's exact element type is known at the store and the stored value provably fits.

open as a page

A row buffer both hands rows out and takes rows in — which member do you drop to make it single-directional?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Drop whichever member puts the placeholder on the side you do not need: remove the accepting member to leave a read-only source, or the handing-back member to leave a write-only sink. One type cannot keep both and stay single-directional.

open as a page

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%

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.

open as a page

A shared policy library's public types: when do you commit to a declared direction rather than narrow at each signature?

level: principalimportance: should knowfreq 30%

basics

~20 s

Commit where the type is genuinely one-directional and will stay so, because a published direction is a one-way door: it constrains the type forever and can break outside implementers. Narrow per signature where the type reads and writes.

open as a page

A source exposes a member taking a callback that receives a row — which position does the row placeholder occupy?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Producer. The placeholder sits in the parameter of a parameter, and each step into a function type's input side flips the direction, so two flips put it back on the output side of the enclosing type.

open as a page

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%

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.

open as a page