skip to content

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

level: middleimportance: should knowfreq 44%

answer

  1. the view promises one direction
  2. input members drop out of reach
  3. reads come back at the upper bound
  4. one occurrence, not the declaration
  5. no wrapper and no copy at run time

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.

solid answer

~50 s

The narrowing is sound precisely because it takes something away. On an occurrence narrowed to a producing view, the compiler either drops every member whose parameter is the element type or types that parameter at an uninhabited type - which amounts to the same thing, since nothing can be supplied for it. Members that return the element type keep working and keep their precise result type. The mirror case is not symmetric: on a consuming view the input members work with any value of the element type, but a member that returns the parameter hands back only the parameter's declared upper bound, so you can retrieve a value and not use it precisely. In both cases the restriction is on that one name's static type. The object is untouched, and another reference to the same store can still do everything the store declares.

code

pseudocode · 14 lines
pseudocode
type RuleSet<T> {
    add(rule: T)                  // T in an input position
    each(): Sequence<T>           // T in an output position
    size(): Number                // does not mention T
}                                 // both positions -> invariant

function audit(set: RuleSet<produces Rule>) {
    for each rule in set.each() { report(rule) }   // allowed: result is a Rule
    print(set.size())                              // allowed: never mentions T
    set.add(defaultRule)                           // rejected: the input member is out of reach
}

audit(strictRules)     // accepted: a RuleSet<StrictRule> is a producing RuleSet<Rule>
strictRules.add(r)     // still legal here: the other reference is unrestricted

go deeper

for a junior

Recall the trade in one line: a narrowed view gains the ability to accept stores over other element types and gives up the members that point the other way.

for a middle

Explain the enforcement, not the intent: the compiler removes the opposite-direction members from that occurrence, which is what makes accepting a differently parameterized store sound.

for a senior

Bring out the asymmetry and the transitivity cost - a consuming view degrades reads to the upper bound, and a narrowed value cannot be handed on without narrowing again.

for a principal

Frame it as an API question: if most signatures over a type need the same view, the repetition is a signal that a one-directional type belongs in the public surface.

## What a narrowed occurrence actually is A read-write store - one that both hands out its elements and accepts new ones - is **invariant**: its parameter appears in an output position and an input position, so no single direction can be promised for the type as a whole. A use site can still promise a direction for **itself**, by narrowing its own view of the store to a producing or a consuming one. The narrowing is a claim about what this occurrence will do with the value, and the compiler makes the claim true by removing the ability to do anything else. That removal is the whole mechanism. Without it the narrowing would be a comment: a signature could announce "I only read" and then write, and the argument it accepted - a store over a subtype - would be corrupted. ## Which members leave, and why On a **producing** view, the parameter is promised to flow outward only: - members that **return** the element type stay callable, and the result has the declared element type, so the caller can use it precisely; - members that **accept** the element type as an input are out of reach. Depending on the system, the member is excluded from the occurrence's interface or its parameter is typed at an uninhabited type; either way there is no value anywhere that can be passed; - members that do neither - a size, a clear, a check for emptiness - are unaffected, because they never mention the parameter. On a **consuming** view the promise runs inward, and the consequences mirror it but are not symmetric: - members that **accept** the element type stay callable with any value of that type or a subtype; - members that **return** the element type stay callable too, but the result is typed at the parameter's declared **upper bound**, because the actual store may be over some supertype and nothing narrower can be guaranteed; - so the consuming view loses **precision on the way out**, while the producing view loses **the way in entirely**. ## Side by side | | producing view | consuming view | |---|---|---| | member returning the parameter | callable, result at the element type | callable, result only at the upper bound | | member accepting the parameter | out of reach, nothing can be passed | callable with the element type or below | | argument it lets the signature accept | a store over any subtype | a store over any supertype | | what the signature gives up | it can add nothing | it cannot use what it retrieves precisely | The left column is the common case in review: a function that walks a store to audit, log, copy out of, or summarise it, and has no business adding anything. The right column is the other half of the same tool, used by a function whose job is to fill a store it did not create. ## What the restriction does not do Three things it is routinely mistaken for: 1. **It is not a run-time wrapper.** Nothing is copied, proxied or frozen. The value passed in is the value the caller held, and after the call the caller still holds it with all its members available. 2. **It is not protection from mutation.** Any other reference to the same store can still write to it, including one held by the very caller that passed the narrowed argument. The narrowing restricts a name, not an object. 3. **It is not transitive by itself.** A value held through a narrowed occurrence cannot be handed on to something expecting the full type - the callee could write, and the compiler has no way to know the element types line up. Passing it further means narrowing there too, which is exactly the repetition the use-site form is known for. ## The read that surprises people The asymmetry in the table is the part interviews probe, because it is the part that cannot be guessed from the phrase "read-only view". A producing view is clean: reads work, writes are gone. A consuming view is not its mirror image - it does not forbid reads, it **degrades** them. You may retrieve an element and you may pass it along as whatever the parameter's upper bound is, and that is often nearly useless, which is why a consuming view is normally taken by code that only writes even though reading was never forbidden. A candidate who says "a consuming view makes reading illegal" has the shape of the idea and the wrong mechanism. A candidate who says "you can read it, but only at the bound, so there is usually nothing worth doing with what comes back" has understood why the two directions lose different things. ## In the interview The answerable core is one sentence and one consequence: the narrowing puts the opposite-direction members out of reach on that occurrence, and it is that removal - not the annotation - that makes accepting a store over a different element type safe.

  • On a consuming view, a member that returns the element type is called. Why is the result not the element type?
    Because the occurrence may be standing for a store over any supertype of that element type. The only thing guaranteed about anything it returns is the parameter's declared upper bound, so that is what the compiler hands back. The value is intact; what is lost is the right to assume anything narrower about it.
  • Does narrowing an occurrence stop other code from mutating the same store?
    No. It restricts what can be done through that name, not what can be done to that object. The caller that passed the argument, and anyone else holding a reference, keeps every member the store declares - including the writes the narrowed signature cannot reach. Immutability is a different mechanism.
  • Can a value held through a producing view be passed to a function that wants the unnarrowed store?
    No, and the rejection is the point. The occurrence may be standing for a store over some subtype, and the callee could write an element that does not belong in it. Passing it on means narrowing the callee's parameter too, which is why the use-site form spreads along a call chain.

saying these in an interview costs you the question

  • Says the narrowed view also blocks writes through other references
  • Thinks the narrowing copies or wraps the value at run time
  • Expects a consuming view to return the exact element type
  • Believes a narrowing at one signature changes the type elsewhere
  • Assumes an out-of-reach input member is merely discouraged, not rejected