Why does a formatter that accepts any value still fit an amount-in, text-out slot, while one returning any value does not?
answer
- two positions, opposite directions
- accepts more, returns less
- parameter position tolerates wider
- result position must not widen
- Liskov substitution through a function type
basics
~10 sThe caller only ever passes an amount and always prints what comes back. So a formatter may accept wider than promised but must return no wider: looser in parameter position, tighter in result position.
solid answer
~40 sBecause the two positions face opposite directions. The renderer only ever **passes** an amount, so a formatter accepting any value is merely over-qualified — every argument it will actually receive is one it already handles. The renderer **consumes** whatever comes back as display text and prints it unexamined, so a formatter returning any value forces the renderer to test and narrow a value the slot promised was already text; that breaks the contract. The rule is looser in parameter position, tighter in result position: a substitute may accept at least what was promised, and must return no more than what was promised. That is the Liskov substitution principle read through a function type.
code
pseudocode · 15 lines// the slot the renderer declares:
// formatter takes an amount, returns display text
// ACCEPTED: accepts any value, returns styled text (a kind of display text)
function describeAny(value)
return styled(asText(value))
end
// REJECTED: accepts only positive amounts, and one branch returns a non-text
function formatPositive(amount)
if amount <= 0 then
return nothing
end
return groupDigits(amount)
endgo deeper
Know that a slot is a promise about the call: an argument of a stated type goes in, a stated type comes back. Which near-misses are still safe is the next layer down.
Explain both directions and why they differ, arguing from the caller's point of view: what it passes, and what it does with the result.
Apply it in review to a real hook: find the implementation that narrowed its parameter type and name the row that will fail because of it.
Judge how much freedom a published hook should grant. An exact match is easiest to check, but the accept-wider, return-narrower allowance is what lets one shared helper serve many slots.
The renderer publishes one slot per column — a **cell-formatter** taking an amount and returning display text — and forty teams supply functions for it. The interesting question is not which functions match the declaration exactly, but which near-misses are still safe, because that is what decides whether a shared helper can serve several slots or every slot needs its own bespoke function. ## The slot is a contract with two sides Read the declaration as two promises rather than one shape: - The renderer promises **to pass an amount**, on every call, and nothing else. - The renderer promises **to treat whatever comes back as display text**, printing it without a test or a conversion. A supplied formatter honours the contract when it can keep the renderer's side of both promises true. Those two promises point in opposite directions, which is why the tolerated mismatches differ between the two positions. ## Parameter position tolerates more A formatter declared to accept any value at all is not a risk. Every argument the renderer will actually pass is an amount, and the formatter already handles amounts — it handles more besides, and the surplus capability is simply never exercised. Nothing in the renderer changes, no conversion happens, and no call can surprise the formatter. Accepting **wider** than promised is free. Narrowing is the dangerous direction here. A formatter accepting only positive amounts cannot honour the promise, because the renderer believes it may pass any amount. The failure does not appear at registration but on the first row carrying a zero or a negative value, which is exactly the kind of defect a type-level rule exists to prevent. ## Result position tolerates less A formatter returning any value at all breaks the second promise. The renderer was told it would receive display text and prints it directly; handed an arbitrary value, it must now test what arrived and decide what to do when it is not text. Work and risk have moved back onto the caller, which is precisely what the declaration ruled out. Returning **wider** than promised is a contract violation. Narrowing is the safe direction here. A formatter returning styled text — a more specific kind of display text — satisfies the renderer completely, because everything the renderer wanted to do with display text it can still do. ## The four combinations | Supplied formatter | Parameter position | Result position | Accepted? | |---|---|---|---| | takes any value, returns styled text | wider | narrower | yes — the safe pair | | takes an amount, returns display text | exact | exact | yes — the declared shape | | takes only positive amounts, returns text | narrower | exact | no — some passed amounts are unhandled | | takes an amount, returns any value | exact | wider | no — the caller must narrow what it was promised | Read the table by the column headings, not by memory of a keyword: a position the caller **produces** values for tolerates a substitute that accepts more, and a position the caller **consumes** values from tolerates a substitute that produces less. ## Liskov substitution, read through a function type The same principle usually stated about a subtype standing in for a supertype appears here with the pieces relabelled. A substitutable implementation may **weaken** what it demands and **strengthen** what it guarantees: it accepts at least what was promised and delivers at least what was promised. In a function type, the demand lives in parameter position and the guarantee lives in result position, which is why the tolerance runs one way in each. Stating it backwards — narrower parameter, wider result — is the single most common error in this material, and it is grammatical enough to survive review. ## What checkers actually do with this Type systems differ on how much of the rule they enforce. Some check both positions in the directions above and accept the safe substitutions silently. Some treat parameter position as invariant, requiring an exact match and rejecting a perfectly safe wider parameter; in those, you write a small adapter to express what the rule would otherwise have allowed. Some perform no static check at all, and the rule survives as a discipline whose violation shows up as a failure on particular rows. The rule itself is the same in all three cases — only who catches the violation changes. ## Where it bites in practice 1. A generic helper that formats anything is written once and dropped into many slots; that is the wider-parameter allowance paying for itself. 2. A team tightens a formatter's parameter type "to be safe" and creates a latent failure for the inputs it no longer accepts. 3. A formatter's result type is loosened during a refactor, and every caller quietly grows a test it never had before.
- A formatter declares a narrower parameter type than the slot — say it accepts only positive amounts. What breaks, and when?The renderer still believes it may pass any amount the slot allows. The first zero or negative amount reaches a function never written for it, so the failure surfaces at call time on particular rows rather than at registration — unless the checker rejects the narrowing up front, which is exactly the value of enforcing the rule.
- Does the same rule apply when the formatter itself takes a function as an argument?Yes, and it reverses once more inside that argument. A function sitting in parameter position is itself in a position that tolerates wider, so within it the tolerance for its own parameter and result flips. Each level of nesting reverses the direction again, which is why deeply nested function types are hard to judge by eye.
A stand-in translator who happens to speak more languages than the job asked for is fine — the extra ones are never called for. One who may hand back the page in any language at all is not, because the reader was promised something they could read.
saying these in an interview costs you the question
- Says a substitute must match both positions exactly.
- Thinks accepting a wider parameter type breaks the caller.
- Allows a wider result because the caller can just check what arrived.
- Gets the direction backwards: narrower parameter, wider result.
- Treats this as a rule about argument count rather than about types.