A row buffer both hands rows out and takes rows in — which member do you drop to make it single-directional?
answer
- one placeholder, one direction
- no rewrite satisfies both sides
- split along the direction of travel
- two surfaces over one object
- the call site decides which member goes
basics
~20 sDrop whichever member puts the placeholder on the side you do not need: remove the accepting member to leave a read-only source, or the handing-back member to leave a write-only sink. One type cannot keep both and stay single-directional.
solid answer
~40 sThere is no member you can rewrite in place. A placeholder that appears as both a result and a parameter is pinned, and the only way out is to stop mentioning it on one side. In practice you split the buffer rather than mutilate it: keep the reading members on a view whose placeholder is output-only, keep the accepting member on a separate surface whose placeholder is input-only, and let the concrete buffer implement both. The nightly renderer then asks for whichever surface it actually uses, and each parameter gets the substitution freedom its direction licenses. What is lost is doing both through one reference — code that genuinely reads and writes has to name the pinned type and gets no freedom from the split.
code
pseudocode · 12 linestype RowBuffer<T>: // a result here, a parameter there: pinned
function take() returns T
function put(value T)
// split along the direction of travel
type RowFeed<T>: // T only a result -> output-only
function take() returns T
type RowDrain<T>: // T only a parameter -> input-only
function put(value T)
type RowBuffer<T> is RowFeed<T> and RowDrain<T> // one object, two directional surfacesgo deeper
Remember that a placeholder mentioned as both a result and a parameter is pinned, and that making it single-directional means removing one of those mentions.
Describe the split concretely: a reading surface, a writing surface, one concrete type implementing both, and what each surface's callers may then pass.
Decide per call site which surface a parameter should take, and say plainly what the split costs the code that genuinely needs both directions.
Judge whether the extra surfaces are worth it across the codebase, since the payoff is freedom for other teams' callers and the cost is a type surface everyone has to learn.
## Why there is no in-place fix The buffer declares `take() returns T` and `put(value T)`. Those two mentions are on opposite sides, and the classification follows from the mentions rather than from intent: as long as both are present, the placeholder is pinned. No annotation, no parameter tweak and no naming convention changes that, because the checker is reading exactly those two signatures. The question therefore has a structural answer, not a clever one — **something has to stop mentioning the placeholder**, and the only choice you get is which side stops. ## Splitting along the direction of travel The usual move is not to delete anything but to redistribute it: - A **feed** type keeps the reading members. Its placeholder appears only as a result, so it is output-only, and a caller may hand it an instantiation over a more specific row type. - A **drain** type keeps the accepting member. Its placeholder appears only as a parameter, so it is input-only, and a caller may hand it an instantiation over a more general row type. - The concrete buffer implements both. At run time there is still one object holding one collection of rows; the split exists in the type surface, not in the data. The renderer then declares the surface it actually uses. A stage that only reads takes the feed; a stage that only writes takes the drain; a stage doing both names the buffer. Each declaration now advertises its direction truthfully, which is what gives its callers room. ## What each surface buys | surface | placeholder's position | what the caller may pass | what the caller may do | |---|---|---|---| | feed | output only | an instantiation over a more specific row type | read rows | | drain | input only | an instantiation over a more general row type | write rows | | buffer | both | the exact instantiation only | read and write | Read down the last two columns and the trade is plain: freedom for the caller is bought with capability on the reference. Every operation you keep on one surface costs the surface some of the substitution freedom it would otherwise have. ## Choosing which member to drop The choice is made at the call sites, not in the abstract: 1. Look at what each parameter of each consuming function does with the value. Reads only, writes only, or both. 2. Where it reads only, the accepting member is the one to drop from that parameter's type — the reads are what the parameter is for. 3. Where it writes only, the handing-back member goes, for the same reason in the other direction. 4. Where it genuinely does both, drop nothing and name the pinned type; forcing a split there just makes the call site take two references to the same object. The mistake to avoid is dropping the member the *type* uses least rather than the one the *parameter* does not need. A buffer whose accepting member is rarely called is not thereby a feed; the classification is about the surface a given parameter is declared with. ## What the caller loses Be honest about the cost in an interview, because a candidate who claims the split is free has not used it: - Code that reads and writes the same buffer through one reference has to name the both-directions type, and gets no substitution freedom at all for its trouble. - The type surface grows: two extra declarations, and a reader has to learn which one each function wants. - A caller that needs both surfaces of the same object either passes it twice or passes the concrete type, which defeats the point for that call. ## What is not in scope here Two neighbouring subjects are easy to drift onto and both belong elsewhere. *Why* a type you can both read and write has to be pinned in the first place — and the classic design that made a built-in container an exception and pays for it with a check at run time — is its own topic. So is the question of where a direction is written down: once on the declaration, or separately at each use, which changes what a split buys you and is the reason some type systems need the split less often than others. The point that belongs here is narrower: given a type with a mention on each side, the single-directional version is obtained by removing one of them, and which one you remove is decided by what the call site does with the value.
- How do you decide which of the two members to drop for a given parameter?By what the function does with that parameter. A stage that only reads rows takes the surface without the accepting member; a stage that only writes takes the surface without the handing-back member. A stage that genuinely does both keeps the pinned type, because forcing a split there buys it nothing.
- Does splitting the type cost anything at run time?Nothing inherent. The concrete buffer is still one object holding one collection; the feed and drain are surfaces onto it. The costs are in the type surface — more declarations to read, and a call site needing both directions has to pass the object as its pinned type or pass two references to it.
- Can a member that mentions the placeholder on the wrong side ever be kept?Sometimes, by restating it so the type's own placeholder is not the one in the offending position — for example typing what arrives with a fresh placeholder related to the type's by a limit. That keeps the operation without the forbidden mention, though type systems differ in whether they offer the shape.
saying these in an interview costs you the question
- Tries to keep both members by weakening the parameter's declared type
- Thinks splitting the type adds an object or a copy at run time
- Assumes code needing both directions still gains substitution freedom
- Drops the member the type uses least rather than the one unused here
- Believes renaming or reordering the members changes the classification