skip to content

A connection type carries a message-type placeholder and a standalone helper carries its own - how long is each fixed?

level: juniorimportance: must knowfreq 62%

answer

  1. scope follows the declaration site
  2. one fixed per instance, one per call
  3. shared name ties members together
  4. no instance means no shared placeholder
  5. used once in a signature constrains nothing

basics

~20 s

A placeholder introduced on the type is chosen once when an instance is created and every member of that instance shares it. A placeholder introduced on one operation is chosen afresh for each call and is scoped to that call.

solid answer

~40 s

The declaration site sets the scope. When the placeholder is introduced on the type header, it is filled in when you build the instance, and from then on the stored state and every operation of that instance all mean the same type by it - a connection made for chat frames sends and receives chat frames for its whole life. When the placeholder is introduced on a single operation's signature, it is filled in per call: the same helper can be called with one type now and another type on the next line, and nothing about the call is remembered afterwards. A free-standing helper, which has no instance behind it, has nowhere else to put a placeholder, so it must introduce its own.

code

pseudocode · 9 lines
pseudocode
type Connection<M>:
    outbox: list of M
    function send(message: M)
    function receive() -> M

function firstOrNone<T>(items: list of T) -> T or none:
    for each item in items:
        return item
    return none

go deeper

for a junior

Recall the one-line rule: a placeholder on the type is picked when the object is built, a placeholder on an operation is picked at each call. Be able to point at which one a given signature uses.

for a middle

Explain why the scope matters: a declaration-level name is what makes stored state and several operations agree on one type, while an operation-level name only has to hold together within a single call.

for a senior

Show that you read the site as an API commitment - how many objects a caller ends up holding, and which agreements between members the compiler is checking for you at no cost.

for a principal

Frame it as an interface-stability choice: where a library puts its placeholders decides whether callers commit once per object or once per call, and moving one later is a breaking change across every call site.

## Two places a placeholder can be introduced A **generic declaration** introduces a name - a **type parameter** - that stands in for a type the code does not yet name. That name has to be introduced somewhere, and there are only two places it can go: on the **declaration as a whole** (the type, and therefore everything the type contains), or on **one operation** inside it (a single method or function signature). The choice looks like syntax and is really about **scope**, and scope decides who has to agree about the type. Take a chat client's connection. Parameterized as a whole, the connection is built *for* a message type: its outgoing buffer holds that type, `send` accepts that type, `receive` produces that type. Parameterized per operation, the connection itself names no message type, and each operation invents one of its own for the duration of a single call. ## What each site means concretely | Introduced on | Chosen when | Stays fixed for | Who can refer to it | |---|---|---|---| | The type declaration | The instance is built | That instance's whole life | Stored state and every member | | One operation | Each call | That one call | Only that operation's signature and body | So a declaration-level placeholder is a promise **between members**: whatever the outbox holds, `send` accepts and `receive` returns. An operation-level placeholder is a promise **inside one signature**: whatever came in as an argument is what comes back out, and the next call is free to be about something else entirely. ## Why a free-standing helper has no choice An operation that belongs to no instance - a helper function on its own, or a member that is not attached to a particular object - has no instance to carry a type argument, so there is nothing for a declaration-level placeholder to be fixed *by*. A helper that returns the first element of a list, or that copies items from one sink to another, therefore introduces its placeholder on its own signature. This is the everyday case that makes the distinction visible: the helper is generic, but nothing about it is remembered between calls. The same reasoning explains a rule that trips people up when they meet it from the other direction: state that is shared by all instances of a type cannot be typed by the type's placeholder, because that placeholder has a different value for each instance, and shared state has only one. ## Reading a signature for its declaration site When you look at an unfamiliar signature, ask three questions in order: 1. **Does the name appear in more than one member?** If the buffer, the sender and the receiver all mention it, it is one shared name, and it belongs on the declaration. 2. **Must two calls agree?** If a value handed to one call has to come back out of a later call on the same object, the placeholder has to outlive a call, so it belongs on the declaration. 3. **Is the name used only within one signature and its body?** Then it belongs to that operation, and hoisting it up would over-constrain every caller. A useful sanity check on the result: an operation-level placeholder that appears **exactly once** in its own signature is usually a mistake, because a name used once constrains nothing - it is a way of writing *any type at all* with extra ceremony. ## What the caller feels - With a declaration-level placeholder, the caller commits **once**, at construction, and then every call is checked against that commitment for free. - With an operation-level placeholder, the caller commits **per call**, and the object it is calling through stays usable for any type. - Mixing them is normal and not a contradiction: a connection fixed to one message type can still expose a request-and-await operation whose *reply* type is chosen per call, because that reply type is genuinely independent of the connection's message type. Languages differ in how the two sites are spelled, and in what they allow an operation to do with a name the enclosing declaration already uses, but the scoping rule itself is the same everywhere: **a name introduced on a declaration lives as long as an instance of that declaration; a name introduced on an operation lives as long as a call.** Everything else about type parameters - how the argument is worked out at a call site, what may be required of it, and what survives compilation - sits on top of this one distinction.

  • Two calls to the same operation-level placeholder pick different types - is that a problem?
    No. Each call is checked on its own, and the name is scoped to the call, so one call may be about frames and the next about acknowledgements. That independence is exactly what the operation-level site is for; if the two calls did have to agree, the name would belong on the declaration instead.
  • Can one declaration use both sites at once?
    Yes, and it is common. A connection parameterized by its message type can still expose an operation that introduces a second, independent placeholder for something the connection does not store - a reply type, a decoded payload, a mapped result. The two names are unrelated, and each keeps its own scope.

A shipping firm that chooses one crate size the day it opens and handles only that size ever after, versus a courier who takes whatever box you hand him for this one trip and forgets it afterwards.

saying these in an interview costs you the question

  • Says a type-level placeholder is re-chosen on every call.
  • Thinks a function outside any type cannot introduce a placeholder of its own.
  • Believes all instances of a parameterized type share one chosen argument.
  • Treats the declaration site as pure syntax with no scope consequence.
  • Assumes an operation-level placeholder is remembered between calls on one object.