skip to content

Forty teams implement your published formatter slot — which edits to its declared type leave every existing implementation valid?

level: seniorimportance: nice to knowfreq 30%

answer

  1. who pays, the slot or the implementers
  2. ask for less, not more
  3. narrow what you pass, widen what you accept
  4. the mirror of the implementer's rule
  5. no substitution rule rescues an arity change

basics

~20 s

Two edits are safe: narrowing the parameter type the slot promises to pass, and widening the result type it will accept back. Both ask implementers for less. Widening the parameter, narrowing the result or changing arity moves work onto all forty teams.

solid answer

~40 s

Two of them. **Narrowing the parameter type** — the renderer now promises to pass less than before — is safe, because an implementation that handled the wider set still handles every value it will now receive. **Widening the result type** is safe for the same reason in mirror: implementations still return what they always returned, and the slot now accepts more. The other two break implementers: widening the parameter hands them values they were never written for, and narrowing the result demands something more specific than they promised. Changing the arity is rescued by no substitution rule at all. Note the inversion — an *implementation* may accept wider and return narrower; the *declaration* may only move the other way.

go deeper

for a junior

Know that the slot's declaration is a contract two sides depend on: the renderer that calls it, and every team that implements it.

for a middle

Take the four possible edits one at a time and say which side is asked to do more — the renderer or the implementers.

for a senior

Plan the rollout: which edits land in one change, which need implementations updated first, and how you stage an arity change without blocking forty teams.

for a principal

Decide whether a hook is published in a form that can evolve at all. A wide parameter and a narrow result leave both safe edits available later, at the cost of harder implementations today.

You own a published cell-formatter slot: one amount in, display text out, implemented by forty teams you do not control. The slot has to change. Which edits can you land without a coordinated migration, and which ones are a migration whatever you call them? ## Two sides, two rules A function type sits between two parties, and the rule for what each may do runs in opposite directions. - An **implementation** may accept wider than the slot promises and return narrower than it promises. It may do more than asked and deliver something more specific. - A **declaration** may promise to pass narrower and promise to accept wider. It may ask for less than before. The two are the same principle seen from opposite ends. Substitution is always safe in the direction of *demanding less and guaranteeing more*, and which concrete edit that corresponds to depends on whether you are the one making the promise or the one meeting it. Reasoning from a remembered rule about implementations, applied to a declaration, gets you exactly the wrong answer — which is why this question separates people who memorised the rule from people who can re-derive it. ## The four edits | Edit to the slot | Effect on existing implementations | Effect on the renderer | |---|---|---| | Narrow the parameter type (pass less) | safe — they already handle the narrower set | it must now guarantee it never passes the excluded values | | Widen the result type (accept more) | safe — what they return is still accepted | every place consuming the result must cope with more | | Widen the parameter type (pass more) | breaks — they meet values they were never written for | none up front; the cost lands on the teams | | Narrow the result type (demand more) | breaks — they return something too general | none up front; the cost lands on the teams | The pattern is worth stating outright: **the two safe edits are the two that move work onto you**, and the two breaking edits are the two that look free from where you sit. That is not a coincidence — it is the whole content of the rule. ## Arity is the edit with no safe direction Adding the row as a second argument is not a widening or a narrowing; it produces a different type. No substitution rule rescues it. An implementation written for the old count survives only where the calling convention happens to discard the surplus, and relying on that turns a contract change into an invisible one. Treat an arity change as a migration from the start. The usual way to run one without freezing forty teams: 1. **Publish the new slot beside the old one**, so nothing breaks on the day of the change. 2. **Adapt at the registration point.** Every implementation still on the old shape is wrapped once, centrally, into the new arity, so the teams' own code need not change immediately. 3. **Move teams across on their own schedule**, with the wrapper as the thing that tells you who is left. 4. **Retire the old slot** when the wrapper has no users, and delete the wrapper with it. ## Choosing the shape you can live with When you first publish a hook you are choosing which future edits will be cheap. A slot declared with a **wide parameter and a narrow result** is demanding of implementers — they must handle everything and produce something specific — but it leaves you room, because both safe edits are still available to you later. A slot declared with a **narrow parameter and a wide result** is easy to implement today and has spent its room: the only edits left are the breaking ones. There is no free choice here, only a decision about who pays and when. The reason it is a senior question is that both answers are defensible and the failure mode is not noticing there was a decision. ## What the renderer pays for the safe edits The safe edits are not costless — they are costless **to implementers**. Narrowing the parameter type means the renderer must now guarantee it never passes the values it has excluded, which may be real work at the call site. Widening the result type means every place that consumed the old, narrower result must now cope with a broader one, and that work is inside your own code where you can do it in one change. That is the trade a published contract buys: keep the cost where you control the schedule.

  • Why is this direction the mirror of the rule for a single implementation?
    An implementation sits on the receiving side of the contract, so it may accept more and guarantee more than asked. The declaration sits on the promising side: narrowing what it passes and widening what it accepts both shrink the promise implementers have to meet. Same principle — demand less, guarantee more — read from the opposite end.
  • What do you do when the arity genuinely has to change?
    Publish the new shape alongside the old, wrap every remaining old implementation once at the registration point so the teams are not blocked, move them across on their own schedule, and retire the old slot when the wrapper has no users left. The wrapper doubles as the inventory of who has not migrated.
  • Does the safe edit cost nothing at all?
    It costs nothing to implementers, which is the point, but the renderer pays. Narrowing the parameter means guaranteeing it never passes the excluded values; widening the result means every consumer of that result must handle a broader type. Both edits live inside code whose schedule you control.

saying these in an interview costs you the question

  • Assumes the rule for implementations applies unchanged to the declaration.
  • Thinks widening the declared parameter type is harmless to implementers.
  • Believes narrowing the promised result type costs implementers nothing.
  • Treats adding a parameter as a small, compatible edit.
  • Forgets that the renderer itself pays for the two safe edits.