A seating chart both reads out seats and accepts new ones - why is such a container safe to vary in neither direction?
answer
- count the doors, not the keywords
- try each direction and see what breaks
- widening ruins the member that takes in
- narrowing ruins the member that hands out
- remove one door and one direction opens
basics
~20 sEach 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.
solid answer
~40 sArgue it member by member. The chart has a `seatAt(i)` that hands out an element and an `assign(i, seat)` that takes one in. If a chart of premium seats could be used as a chart of seats, `assign` would accept any seat and put a plain one into premium storage. If a chart of seats could be used as a chart of premium seats, `seatAt` would hand back a plain seat to a caller whose type promises premium. Widening breaks the writing member; narrowing breaks the reading member. Since both members exist on the same type, neither direction survives, and the type is usable only at its own element - which is what invariance means.
code
pseudocode · 11 linestype SeatChart<T>
function seatAt(i) -> T
function assign(i, seat: T)
// attempt 1: chart of PremiumSeat used as chart of Seat
wide: SeatChart<Seat> = premiumChart
wide.assign(3, makeStandardSeat()) // a plain seat lands in premium storage
// attempt 2: chart of Seat used as chart of PremiumSeat
narrow: SeatChart<PremiumSeat> = anyChart
p = narrow.seatAt(3) // hands back a plain seat typed premiumgo deeper
Recall that a container you can both read and write is usable only at its own element type, and that reading and writing fail for opposite reasons.
Walk both directions out loud: which member breaks, what value reaches which caller, and why removing one member re-opens exactly one direction.
Show the design move this implies - publish a read-only surface for readers and keep the writable container at its own element type, and say what maintaining that second surface costs.
Decide it at the API level: which containers your codebase exposes at all, which of them cross module boundaries as read-only views, and what you are willing to pay for the extra surfaces.
## The chart has two doors Model the seating chart as a container with exactly two interesting members: - `seatAt(index)` - hands an element **out**. - `assign(index, seat)` - takes an element **in**. Variance is the question of whether a chart over a narrower element may stand in for a chart over a wider one, or the reverse. The honest way to answer it is not to consult a mnemonic but to try each direction and see which member breaks. ## Direction one: widening, and the writing member breaks Suppose a chart of `PremiumSeat` were usable wherever a chart of `Seat` is expected. The receiving code holds a chart of `Seat`, so its `assign` is typed to accept any seat, and it writes a standard seat at index 3. The storage was created for premium seats and its original owner still reads it as premium-only. The `seatAt` on the owner's side is now capable of returning a value that its own signature says is impossible. The reading member is untroubled by this direction: every premium seat genuinely is a seat, so reading through the wider chart returns something that fits. **Widening costs you the writing member.** ## Direction two: narrowing, and the reading member breaks Now suppose the opposite - a chart of `Seat` usable wherever a chart of `PremiumSeat` is expected. Here `assign` is fine: the receiving code will only ever hand it premium seats, and premium seats are perfectly acceptable contents for a chart of seats. But `seatAt` is now typed to return a `PremiumSeat` while the storage may hold any seat at all, so the very first read can hand back a plain seat to a caller who will ask it for something only premium seats offer. **Narrowing costs you the reading member.** ## The two directions side by side | Direction attempted | `seatAt` (hands out) | `assign` (takes in) | Verdict | |---|---|---|---| | Chart of premium used as chart of seats | safe - every element is a seat | broken - a plain seat gets stored | rejected | | Chart of seats used as chart of premium | broken - a plain seat is returned as premium | safe - premium seats are seats | rejected | | Chart used at its own element type | safe | safe | the only sound option | Both candidate directions are eliminated by a different member, and there is no third direction to try. The type is therefore usable only at its own element - it is **invariant**. Note what did the work: not mutability as a mood, but the simultaneous presence of a member that produces the element and a member that consumes it. ## Why immutability changes the answer Remove `assign` and direction one stops breaking: a chart that only hands elements out can be used where a chart of a wider element is expected, because every element it can produce already satisfies the wider type. Remove `seatAt` instead - a write-only sink - and direction two stops breaking. It is the **combination** that forces the strict answer, which is why so many containers that feel like they ought to vary do not: almost every useful mutable container has both doors. ## The escape: hand out one door When the calling code genuinely only reads, you do not have to choose between an unsound widening and refusing the call. Publish a narrower surface: a read-only view over the same storage that exposes `seatAt` and no `assign`. The view has only the producing member, so it may safely widen, and the underlying chart stays usable at its own element type for whoever still needs to write. The cost is that somebody must design and maintain that second surface, and that a caller holding the view cannot write even where writing would have been fine. ## Saying it in an interview Do not answer "because it is mutable". Answer with the two worked breakages: name the member that fails in each direction and the caller it betrays. That is a claim an interviewer can check in thirty seconds, and it generalises to any container you are shown - count the doors, try both directions, and let the members decide.
- The calling code only ever reads from the chart. What do you hand it instead of widening the chart itself?A read-only view over the same storage that exposes the producing member and not the consuming one. With only one door it may safely stand in for a view over a wider element, while the chart itself stays usable at its own element type for writers. You pay by maintaining the extra surface.
- Does a member that both takes and returns the element type change the analysis?No - it makes it stricter in a single member. Such a member has the element in both an incoming and an outgoing position at once, so it alone rules out both directions. A container needs only one of those to be pinned to its own element type.
- Why does an immutable container still fail to vary in the narrowing direction?Because removing writes only rescues the direction that writes broke. An immutable chart of seats still cannot pose as a chart of premium seats: its reading member would promise premium and deliver whatever it holds. Immutability buys widening, never narrowing.
saying these in an interview costs you the question
- Says mutability alone forbids it, without naming a member
- Claims the reading member is what widening breaks
- Thinks narrowing is blocked by the writing member
- Believes an immutable container may vary in both directions
- Says a run-time check makes either direction sound
- Treats the strict answer as a compiler limitation to work around