A shared policy library can state a type's variance once on the declaration or let each caller narrow it - what does each buy?
answer
- same rule, different place
- once by the author, or repeatedly
- one check over every member
- invariant type, narrowed per signature
- a forgotten narrowing rejects safe arguments
basics
~20 sDeclaration-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.
solid answer
~50 sBoth express the same rule - whether a parameterized type over a subtype may stand in for one over a supertype - but at different sites. With a direction written on the declaration, the type's author says it once; the compiler checks at that point that every member uses the parameter only in the permitted position, and from then on every occurrence anywhere gets the substitution for free. With use-site narrowing the declaration stays invariant and each signature that wants flexibility narrows its own occurrence to a producing or consuming view. The author's form is free for callers and checked in one place, but it is unavailable for a type that both accepts and returns the parameter. The caller's form works on exactly those types, at the cost of restating the direction in every signature that needs it - and a signature that forgets it rejects arguments that were perfectly safe.
code
pseudocode · 14 lines// site 1: the author writes the direction once, on the declaration
type Feed<produces T> {
next(): T
}
function drain(f: Feed<Event>) // a Feed<LoginEvent> is accepted; the caller writes nothing
function forward(f: Feed<Event>) // and so is this one, for free
// site 2: the declaration stays invariant, every use states it again
type Channel<T> {
next(): T
push(value: T)
}
function drain2(c: Channel<produces Event>) // narrowed here: a Channel<LoginEvent> is accepted
function forward2(c: Channel<Event>) // omitted here: the same argument is rejectedgo deeper
Recall that the direction can be written by the type's author on the declaration, or by a caller on one occurrence, and that both are decided entirely at compile time.
Explain the check behind each: at the declaration the compiler verifies every member against the direction once; at a use it restricts that occurrence and leaves the declaration untouched.
Show what the difference costs a shared library: an omitted narrowing is silent until a consumer is rejected, and that consumer cannot fix the signature it was rejected by.
Weigh permanence against reach: a declared direction is a published promise over the type's whole future, while a narrowing stays local, reversible, and has to be remembered every time.
## The same rule, two places to write it **Variance** is one question about a parameterized type: when one type argument is a subtype of another, is the parameterized type over the first a subtype of the parameterized type over the second? A type system cannot guess the answer safely from the name, so it has to be told. There are exactly two places the answer can be written: on the **declaration**, by the type's author, once; or at the **use**, by whoever writes a signature, field or local that mentions the type - and there, once per occurrence. Both sites express the same fact. What differs is who states it, how often it has to be stated, and what the compiler can check at the moment it is stated. ## The author states it once When the direction is written on the type parameter itself, the compiler performs a single check at the declaration: it walks **every member** of the type and confirms the parameter appears only in positions the direction permits - output positions for a producing direction, input positions for a consuming one. A violation is one compile error, in the author's own file, before anything ships. Once that check passes: - every occurrence of the type, in every file and every consuming codebase, gets the substitution automatically; - callers write nothing, so they cannot forget to write it; - a signature added years later inherits the flexibility without anyone remembering why; - the rule lives in one place, so there is one thing to read and one thing to change. The costs are **reach** and **permanence**. Reach, because the promise is over all members at once: a type whose parameter appears in both an input and an output position cannot carry any direction at all, and no amount of annotation will change that. Permanence, because the direction is published: once outside code depends on the subtyping it grants, the type may never gain a member that uses the parameter in the opposite position. ## The caller states it per occurrence When the declaration stays invariant, a use site narrows **its own view** of the type to a producing or a consuming one. The compiler then treats that occurrence as if the parameter carried that direction, and correspondingly puts out of reach the members that would break the promise - which is what makes the narrowing sound rather than a request to be trusted. What that buys: - it works on types that can never carry a declared direction, including anything that both accepts and returns the parameter; - one signature may choose the direction this use actually needs while another signature over the same type chooses the other; - nothing is promised about the type itself, so nothing is committed to and the narrowing can be removed later without touching a single consumer. The cost is **repetition and silence**. Every signature that wants the flexibility has to say so; a signature that omits it is not an error anywhere - it simply rejects arguments that would have been safe, and it rejects them at the caller, who cannot restate the direction from outside. The omission is invisible until someone tries. ## Side by side | | direction on the declaration | narrowing at the use | |---|---|---| | who writes it | the type's author, once | every signature that needs it | | when it is checked | at the declaration, over all members | at that occurrence only | | where a mistake lands | one error, in the author's file | a rejected argument at each call | | works on a type used in both directions | no | yes | | reversible | no, once published | yes, local to the occurrence | | effort for the caller | none | one narrowing per occurrence | ## What neither of them does Neither site does anything at run time. Both are judgments about which assignments the compiler will accept; no value is converted, copied or wrapped, and the object reached through a narrowed occurrence is the same object reached through any other reference to it. A narrowed view is a restriction on **this name's static type**, not a protective shell around the value. Languages differ in which sites they offer: some provide only the declaration form, some only the use form, and some both, in which case a caller may narrow an occurrence of a type that is already invariant but has nothing to gain by narrowing one whose direction is already declared. The mechanism is the same everywhere it appears; only the availability differs. ## What an interviewer is listening for The weak answer is "the same thing with different syntax". The two facts that separate them are the ones to say out loud: a declared direction is **unavailable** for a type used in both directions, and a use-site narrowing is **silent when omitted**. Everything else - who types more, where the error lands, whether the decision can be taken back - follows from those two.
- The author has already declared the parameter as producing. Can a caller still narrow that same occurrence?There is nothing left to gain. The declared direction already caps what any occurrence can do: members that take the parameter as input are out of reach everywhere, so a producing narrowing is redundant, and a consuming one is refused because the declaration promised the opposite. Narrowing is the tool for occurrences of types that are still invariant.
- Does writing the direction on the declaration cost the callers anything they could previously do?It constrains the type's future rather than the callers' present. Once published, no member may ever use the parameter in the opposite position, so an input-taking member can never be added over it. Plain callers only gain: assignments that did not compile before now do, and none that compiled stop.
- Why can the compiler check a declared direction all at once, but not a missing narrowing?The declaration is a closed list - every member of the type is visible at that point, so the check is complete. A missing narrowing is not a defect in any one file: the signature is well formed and the type is well formed, and the only evidence is an argument someone somewhere tried to pass and could not.
One rule posted at a building's entrance binds every visitor and can never be forgotten. A notice pinned on each door has to be written again for every room, but it can say something different where the traffic in that room runs both ways.
saying these in an interview costs you the question
- Calls the two sites interchangeable, just different syntax
- Thinks a caller must restate a direction the author already declared
- Claims a narrowing at one signature flexes the other signatures too
- Treats either form as a conversion performed at run time
- Says a type used in both directions can still carry a declared direction