Why must a connection's outbox, send and receive share one declaration-level placeholder rather than each introducing its own?
answer
- one name, several members
- agreement is the point
- scope must cover the field too
- independent names relate nothing
- count members sharing the name
basics
~20 sOnly a placeholder introduced on the declaration is one name that all members mean the same thing by. Give each operation its own and nothing ties them together: a value put in and a value taken out need never match, and the stored buffer agrees with neither.
solid answer
~40 sA placeholder on the declaration is a single name in scope for the whole type, so the buffer's element type, the argument of `send` and the result of `receive` are checked against one another at compile time. That is the guarantee callers rely on: what goes into this connection is what comes back out of it. If each operation introduced its own placeholder, each call would still be internally consistent, but the compiler would have no reason to relate one call to another or to the stored buffer - the buffer would have to fall back to some universal element type and the operations to unchecked conversions. Independent names are the right choice only when the values genuinely are independent.
code
pseudocode · 11 linestype Connection:
outbox: list of anything
function send<M>(message: M):
outbox.add(message)
function receive<R>() -> R:
return narrow(outbox.removeFirst())
type BetterConnection<M>:
outbox: list of M
function send(message: M)
function receive() -> Mgo deeper
Remember that a name introduced on the type is visible to all its members, and that is what lets stored state and operations be checked against each other.
Explain the mechanics: scope decides what can be related, a field cannot be typed by a per-call name, and independent names leave a universal element type plus a narrowing behind.
Demonstrate the review instinct - spot a universal-typed field next to per-call placeholders, and explain which guarantee the callers silently lost when the names were split.
Weigh the API commitment: sharing a name binds callers to one type per object, so decide it when the interface is designed rather than discovering it through a breaking change later.
## The question behind the syntax When several members of one declaration all deal in the same unknown type, you can introduce that unknown **once on the declaration** or **once per operation**. Both compile. Only one of them expresses the thing you actually meant. Consider a chat client's connection with three pieces: an outbox holding queued messages, a `send` operation that appends to it, and a `receive` operation that produces the next inbound message. The domain fact is that these three are **about the same kind of value**. Parameterizing the declaration states exactly that fact in the signature. ## What one shared name buys With the placeholder on the declaration: - The outbox is a list **of that name**, so nothing of another type can be queued. - `send` accepts **that name**, so its argument is checked against the outbox's element type without any conversion. - `receive` returns **that name**, so a caller who built the connection for chat frames gets a chat frame back with no test and no cast. - Every one of those checks happens once, at compile time, and costs nothing at run time. The key phrase is **agreement between members**. A type parameter used by one member alone constrains that member. A type parameter used by several is a *relation* the compiler now enforces across them. ## What per-operation names lose If instead `send` introduces its own name and `receive` introduces another, each signature is still perfectly well typed - and completely uninformative: 1. A value sent on one call and a value produced by the next have no declared relationship at all. 2. The outbox cannot be typed by either name, because neither name is in scope at the field; it has to hold some universal element type instead. 3. Reading out of that outbox therefore needs a narrowing step that the compiler cannot verify, which is precisely the situation parameterization existed to remove. 4. A caller reading the signatures learns nothing about the object, only about individual calls. | Placeholder on | Buffer can be typed by it | send/receive related | Caller commits | |---|---|---|---| | The declaration | Yes | Yes, by the same name | Once, at construction | | Each operation | No | No | Once per call | ## The test to apply The rule is short: **if two things must agree, the name that expresses them must be in scope for both.** Scope is the whole mechanism. A name introduced on an operation goes out of scope when the call ends, so it cannot be the thing that relates a field to a call, or one call to another. Run the test on the three members above and the answer is forced. Now run it on a fourth member - a request-and-await operation that sends a request and yields a decoded reply. The reply type is *not* the connection's message type and is not stored anywhere; it is chosen by whoever makes the request. Nothing else has to agree with it, so it takes its own operation-level name, and doing so is not an inconsistency: the same declaration correctly carries both kinds of placeholder at once. ## Failure modes at both ends - **Pushed down too far.** A name that should relate members becomes several unrelated names. The code still compiles; the guarantees quietly disappear and unchecked conversions creep back into bodies that used to need none. - **Pulled up too far.** A name that only one operation cared about becomes part of the type, and every caller now commits to it at construction whether or not they use that operation. Both mistakes are invisible to a test suite, because a wrong declaration site does not change what the code *does* on a happy path - it changes what the compiler is able to prove about it. The only way to catch them is to read the signature and ask which members share the name. ## The cheap summary A placeholder on the declaration says *this object is about one type*. A placeholder on an operation says *this call is about one type*. Choose by counting how many members have to agree: more than one, and the name belongs upstairs; exactly one, and it belongs where it is used. Languages spell both sites differently, and some let one hide the other, but the scoping rule that decides which you need does not vary.
- Is it ever right for one member of a parameterized declaration to introduce its own placeholder as well?Yes, whenever that member's unknown is genuinely unrelated to the declaration's. A request-and-await operation whose reply type is picked by the caller, or a transform that maps stored values to some other type, both need a second name scoped to the call. The declaration's name still governs everything that is stored.
- How would you spot this mistake in an existing API during review?Look for a field typed by a universal element type sitting beside operations that each declare their own placeholder, and for bodies that narrow values coming out of that field. That pairing means a relation between members was expressed as unrelated per-call names, and the narrowing is the cost.
- Does sharing one placeholder across members cost anything at run time?No. The agreement is checked while compiling; it constrains what may be written, not what happens when it runs. The cost is in flexibility, not speed: every member of that instance is now committed to one type, which is the point when they genuinely must agree.
saying these in an interview costs you the question
- Says two independent placeholders are related if they happen to be filled alike.
- Believes the checking still happens when each operation declares its own name.
- Thinks a field can be typed by a name introduced on an operation.
- Claims a shared placeholder costs something at run time.
- Treats per-operation names as always the more flexible choice.