skip to content

A source exposes a member taking a callback that receives a row — which position does the row placeholder occupy?

level: seniorimportance: nice to knowfreq 26%

answer

  1. read positions recursively
  2. count the flips down the path
  3. a parameter of a parameter cancels
  4. a result of a parameter does not
  5. verify with a substitution walk

basics

~20 s

Producer. The placeholder sits in the parameter of a parameter, and each step into a function type's input side flips the direction, so two flips put it back on the output side of the enclosing type.

solid answer

~40 s

Read positions recursively rather than at depth one. A member's own parameter is an input position, but when that parameter is itself a function, stepping into its parameters flips the direction again while its result keeps it. So a member `subscribe(handler)` whose handler receives a row puts the row placeholder back in an output position: the enclosing type is still the side producing rows, it has simply chosen to push them into a callback instead of returning them. Check it with a substitution — a source over a narrower row type can safely be given a handler written for a wider one, because that handler already accepts every row the source will hand it. The mirror shape, a member taking a factory that returns rows, lands on the input side.

code

pseudocode · 8 lines
pseudocode
type RowSource<T>:
    function nextRow() returns T                 // result              -> producer
    function subscribe(handler function(T))      // parameter of a parameter -> producer
    function fill(factory function() returns T)  // result of a parameter    -> consumer

// substitution walk on subscribe, with SalesRow a kind of ReportRow:
//   a source of sales rows may be given a handler written for report rows,
//   because that handler already accepts every sales row it is handed.

go deeper

for a junior

Know that a placeholder buried inside a parameter still counts, and that its direction is not decided by the outermost step alone.

for a middle

State the flip rule and apply it to both shapes: a member taking a callback that receives the placeholder, and one taking a factory that returns it.

for a senior

Spot the factory-shaped parameter in review that quietly pins a type everyone treated as output-only, and confirm it with a substitution walk before arguing about it.

for a principal

Decide whether a push-shaped surface is worth publishing so the type stays single-directional, given how much caller freedom that preserves across teams.

## Positions are read recursively The simple reading — results are outputs, parameters are inputs — is correct, but it is a reading of one level. Type arguments nest, and the position of a placeholder is determined by the whole path from the type's boundary down to the mention, not by the outermost step. The rule that makes it tractable is small: **each time the path passes into the parameter side of a function type, the direction flips; passing into a result keeps it.** Count the flips from the enclosing type down to the mention, and an even count means the mention is on the output side while an odd count means it is on the input side. This is why a member that hands rows to a supplied callback does not break an output-only claim. The path is: into the member's parameter (one flip), then into that function's parameter (a second flip). Two flips cancel, and the mention lands where it started — output. ## Counting the flips | member shape | where the placeholder sits | flips | position for the enclosing type | |---|---|---|---| | `nextRow() returns T` | a result | 0 | producer | | `accept(value T)` | a parameter | 1 | consumer | | `subscribe(handler function(T))` | a parameter of a parameter | 2 | producer | | `fill(factory function() returns T)` | a result of a parameter | 1 | consumer | The last two rows are the pair worth memorising, because they look symmetrical and are not. A callback *receives* rows — so the enclosing type must be the one supplying them, and it is a producer. A factory *supplies* rows — so the enclosing type is the one receiving them, and it is a consumer. The flip count reproduces exactly that intuition, which is the point: the rule is not an arbitrary bit of bookkeeping, it is the direction of travel followed through one more layer. ## Checking it with a substitution When the counting feels like a trick, verify it the slow way. Take sales rows to be a kind of report row, and ask whether a source over sales rows may stand in where a source over report rows is expected, given only the callback-taking member: 1. The caller holds what it believes is a source of report rows and calls the member with a handler written to accept report rows. 2. The object underneath is a source of sales rows, and it hands the handler a sales row. 3. A sales row is a report row, so the handler accepts it and every operation it performs is defined. Nothing breaks, which is the same verdict the flip count gave: producer position, safe with a more specific argument. Now run the same walk for the factory-taking member and it does break — a source over sales rows would receive report rows from a factory typed for them, and those are not all sales rows. Odd flip count, consumer position, unsafe in that direction. ## Why this matters outside a puzzle Callback-shaped members are how a great deal of real code moves values: a push-style source, a visitor handed to a traversal, a comparison function handed to an ordering operation, a handler registered with an event surface. If positions were read only at depth one, every one of those types would look like it consumed its placeholder and would lose the substitution freedom it is actually entitled to. Type systems read recursively for that reason, and library authors rely on it: a push-style source and a pull-style source can both be output-only, even though one of them mentions its placeholder inside a parameter. The mirror case matters just as much in review. A member that accepts a factory, a supplier, a default-value producer or a deserialiser quietly puts the placeholder on the input side, and it is the single member most likely to pin a type that everyone assumed was output-only. It is easy to miss precisely because it *looks* like the type is receiving a function rather than receiving rows. ## Where it goes wrong - **Stopping at the first parameter list.** The most common error: seeing the placeholder inside a parameter and declaring the type a consumer without going a level deeper. - **Counting the flips but forgetting the result side.** Stepping into a function's result changes nothing; only its parameter side flips. - **Reasoning from the body.** What the member does with the value is irrelevant; the signature settles the position. - **Assuming a function-typed member is somehow exempt from the reading.** It is read like any other member, just with a longer path. - **Trusting the count without a sanity check.** When the shape is unfamiliar, run the substitution walk and look for the call that breaks; the two methods should agree, and if they do not, the count was miscounted.

  • Why does stepping into a function type's result not flip the direction?
    Because the value still travels the same way. A function's result leaves that function, and if the function itself is something the enclosing type receives, what it returns arrives at the enclosing type — so the direction is carried through unchanged, and only the parameter side reverses it.
  • What happens with a member taking a function that both receives and returns the placeholder?
    The placeholder then has one mention at an even flip count and one at an odd count, so it appears on both sides and the enclosing type is pinned in it — exactly as if the type had one returning member and one accepting member at the top level.
  • Does the same counting apply to placeholders nested in ordinary type arguments?
    Yes, with the nested type's own direction carried along the path. A placeholder inside an output-only container in a result position stays an output; inside an input-only container in a result position it lands on the input side, because that container reverses what it is handed.

saying these in an interview costs you the question

  • Stops counting at the member's own parameter list
  • Says any callback parameter makes the enclosing type input-only
  • Thinks a function-typed member is exempt from the position reading
  • Counts the flips but also flips at a function's result
  • Decides from what the body does rather than from the signature