Why is invariance the default answer for a parameterized roster type rather than an inferred direction?
answer
- what does a wrong guess cost?
- two failure modes, different blast radius
- sound whatever the container offers
- the author knows the intent
- loosen later, never tighten later
basics
~20 sInvariance 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.
solid answer
~50 sSubstitution is a promise about a container's whole surface, and only the author of that container knows what the surface is meant to be - now and after the next method is added. A type system that guessed a direction would sometimes guess a permissive one and admit calls it cannot justify, and that failure is silent. Invariance never admits a substitution that a container cannot support, so it is the default that is sound regardless. Its cost is a false rejection: a perfectly safe call refused until someone states a direction. That trade is deliberate, because a rejected program is a compile error you can read, while a wrongly accepted one becomes a defect somewhere else. It is also the compatible direction to start from - declaring a direction later generally keeps existing calls compiling, whereas removing one would break them.
go deeper
Remember that nothing is assumed about a container's relation until someone states it, and that the strict answer is what you get by default.
Explain the asymmetry between a false rejection and a false acceptance, and why the strict answer is sound no matter what the container offers.
Show that you price the default honestly: name the copy and the lost aliasing that a refused substitution forces on callers.
Own the ordering across teams - publish strict, loosen deliberately, and treat a direction as a contract you cannot quietly withdraw later.
## Two ways a type check can be wrong Any rule about substitution can fail in two directions. It can **reject a program that would have been fine** - a false rejection, which shows up immediately as a build error at a specific line, annoying and trivially diagnosable. Or it can **accept a program that is not fine** - a false acceptance, which shows up later, somewhere else, as behaviour nobody can trace back to the substitution that allowed it. The two failures are not symmetric in cost, and the default answer for a parameterized container is chosen with that asymmetry in mind. ## Why not just work it out? The obvious objection is that the checker could look at the container and derive a direction. Some type systems do exactly that, and others require the author to declare it - the systems genuinely differ here, and knowing that they differ is part of the answer. But there is a reason the strict answer is the default in the absence of information: - **A direction is a claim about the whole surface**, not about one operation. Whether it holds is decided by everything the container offers, so nothing local justifies it. - **The surface changes.** A container that supports a permissive direction today may stop supporting it the moment a new operation is added. If the direction were silently inferred, adding an operation could silently retract a relation that callers depend on, and the error would land in someone else's file. - **Only the author knows the intent.** Whether a container is meant to be a narrow view or a general one is a design decision. A checker can see what the code does; it cannot see what the code is for. ## Invariance is the universally sound answer Invariance relates no two distinct instantiations. Because it admits only the exact element argument - plus ordinary subtypes of the container that keep that argument - it can never admit a substitution the container cannot support, whatever that container turns out to offer. Covariance and contravariance are each sound for some containers and unsound for others; invariance is sound for all of them. That is what makes it usable as a default: the checker does not have to be right about the container to be safe. ## The compatibility argument There is a second, practical reason to start strict, and it is the one that matters to whoever maintains a shared container type. 1. **Starting invariant and adding a direction later** widens what call sites may pass. Existing calls passed the exact argument, and they still do, so they generally keep compiling. (Generally, not always: widening what a signature accepts can shift how a call is resolved or how a type argument is inferred, so it is a change worth building against, not a guaranteed no-op.) 2. **Starting permissive and tightening later** narrows what call sites may pass, and every caller that relied on the looser rule breaks at once. A default you can loosen is friendlier than one you must tighten. That ordering is why library authors are advised to publish the strict answer until they have a reason and a design for a looser one. ## What the default costs It is not free, and pretending otherwise is a weak answer. Under an invariant default: - Callers holding a roster of a specific vehicle kind cannot pass it to a routine over the broader element type, even when the routine would only ever have done safe things with it. - The workaround - building a fresh container over the expected element type - costs a pass over the elements and, more importantly, breaks aliasing: later changes to one container are no longer visible through the other. - Signatures accumulate looseness that has to be written down explicitly, which is more text and one more thing for a reader to interpret. Those are real costs paid in exchange for the guarantee that no substitution slips through unexamined. ## Is invariance ever what you actually want? Yes, and that is the mark of a candidate who understands the default rather than tolerating it. A container meant to be handed round and updated in place is often deliberately invariant, because the exactness of its element argument is part of what it promises its users. In that case the default is not a placeholder waiting to be relaxed; it is the answer. ## How to answer this Lead with the asymmetry of the two failure modes, then say that invariance is the answer that stays sound whatever the container offers, then close on compatibility: it is the direction you can loosen later. Mentioning that some systems derive a direction instead of defaulting shows you know the space rather than one habit.
- Is invariance ever the answer you want deliberately, rather than a default you failed to relax?Often. A container whose exact element argument is part of its promise - one that callers share and update in place - is deliberately invariant, and relaxing it would weaken the guarantee its users rely on. Treating every invariant container as unfinished is a misreading.
- What does a caller pay when the default rejects a call that would in fact have been safe?A pass over the elements to build a container over the expected element type, plus the loss of aliasing: the two containers are now independent, so later changes to one are invisible through the other. For a large or streaming source, that cost can be prohibitive.
saying these in an interview costs you the question
- Saying invariance is the default because it is cheaper to check
- Claiming a looser direction is unsound for every container
- Believing a declared direction is always a drop-in change for callers
- Treating every invariant container as a mistake waiting to be relaxed
- Asserting that no type system ever derives a direction automatically