Your registry requires a handler declared over exactly the route's payload type, and a working shared handler is rejected — what over-constrained the signature?
answer
- the signature asked for too much
- identity where acceptance would do
- match the relation to the body
- the caller must worsen its own type
- loosening admits, never breaks
basics
~20 sStating the relation between the two placeholders as identity rather than as what the body needs. The body only requires that the handler accept the route's payload; demanding the handler be declared over that exact type rejects broader handlers that already accept it.
solid answer
~50 sThe signature relates its two placeholders with the strongest claim available — the handler's payload placeholder must *be* the route's declared payload — when the body never needed that much. What the body actually does is hand the route's payload to the handler, which only requires that the handler **accept** values of that type. A shared handler written over a broader payload accepts them and more besides, and is rejected purely because its declared type is not identical. Restating the relation as "the route's payload must be acceptable to this handler" keeps the pairing honest and admits the handler. The rule generalises: with a dependent pair, state the weakest relation the body actually depends on, because every strengthening is a set of callers you have refused. How a parameterised consumer relates to sub- and supertypes of its own argument is a separate subject; the choice of relation is the part that belongs to the signature.
code
pseudocode · 14 lines// as written: the handler must be declared over exactly this route's payload
function register<Route, Payload>(r: Route, h: Handler<Payload>)
requires Payload is the payload type that Route declares
auditHandler: Handler<AuditablePayload> // takes any auditable payload
orderRoute declares OrderPayload, and every OrderPayload is an AuditablePayload
register(orderRoute, auditHandler) // rejected: AuditablePayload is not OrderPayload
// restated to what the body needs: the route's payload must be acceptable to the handler
function register<Route, Accepted>(r: Route, h: Handler<Accepted>)
requires the payload type that Route declares is acceptable to Handler<Accepted>
register(orderRoute, auditHandler) // accepted; a narrower handler is still refusedgo deeper
Note the idea for later: a signature can refuse a value that would have worked perfectly. When that happens the signature, not the call, is usually what needs looking at.
Be able to compare the relation a signature states against what its body actually does with each placeholder, and to name the weaker relation that would still be enough.
Diagnose it in review. Spot the tell — a caller forced to make its own type worse to satisfy a registration — trace it to the relation, restate it at the body's strength, and confirm existing callers are unaffected.
Own the default for published surfaces: relations start at the weakest the body supports, and each strengthening is argued and recorded, because tightening later is the direction that breaks teams you cannot redeploy.
## The rejection The registry pairs a route with the handler that will receive its payload, and states the relation between the two placeholders as identity: ``` function register<Route, Payload>(r: Route, h: Handler<Payload>) requires Payload is the payload type that Route declares ``` A team then writes one auditing handler over a broad payload type — every payload that carries audit fields — and registers it on a dozen routes. Each route declares its own narrower payload, and each of those payloads is one of the broad ones. Every registration is rejected. The handler would work: hand it any of those payloads and it does the right thing. The signature refuses it anyway, and the error blames the caller for a decision the signature made. ## Identity is the strongest relation available Between two placeholders you may state a spectrum of relations, and identity sits at one end of it. | Relation stated between the pair | Which handlers are admitted | Which are refused | When it is the right choice | |---|---|---|---| | The handler's payload **is** the route's payload | only the exactly-matching handler | every broader handler that already works | the body needs to both give and take that exact type | | The route's payload is **acceptable to** the handler | the matching handler and any broader one | handlers too narrow to take the route's values | the body only passes the payload in | | No relation at all | every handler | none | there is no real pairing — usually a design error | The first row is not wrong in general. It is wrong **here**, because the body of `register` and everything downstream of it only ever moves a payload in one direction: from the route to the handler. Identity is what you state when values travel both ways. ## Find the weakest relation the body depends on The diagnosis is a mechanical exercise, and it is worth doing aloud in an interview. 1. **List what the body does with each placeholder.** Here: it stores the route, and it will later call the handler with a value of the route's payload type. 2. **Write down the requirement each of those operations actually imposes.** Calling the handler with that value requires only that the handler accepts it. 3. **Compare that with the relation the signature states.** Identity is strictly stronger than acceptance, so the gap between the two is exactly the set of callers being refused for no reason. 4. **Restate the relation at step 2's strength** and re-check every existing call site still compiles — weakening a relation never breaks an existing caller, it only admits more. Step 4 is the reassuring part: loosening a dependent pair is a safe edit in a way that tightening one never is. ## Three fixes that are worse than the problem - **Widening the handler's own declaration** so its payload type matches this route exactly. This makes one call site compile by making the handler less reusable everywhere else, and it does not scale past the second route. - **Dropping the second placeholder** and passing the payload through a common supertype, with the body checking at run time. That deletes the pairing the registry existed to enforce and converts a rejected call into a production failure. - **Keeping identity and adding an escape hatch** — a body-level conversion or an unchecked pass-through for the cases the checker refuses. That leaves the over-constrained signature in place as the documented contract while the code quietly violates it. All three treat the signature as fixed and the callers as the problem. The signature is the thing you control. ## Reading over-constraint in review A dependent pair is over-constrained when the relation it states is stronger than the operations in the body require, and the symptom is always the same shape: - a caller with a value that demonstrably works is refused; - the refusal names a type relationship rather than a missing capability; - the caller's workaround is to make its own type worse. That last symptom is the reliable one. When accommodating a signature makes a caller's code less useful to its other users, the signature asked for too much. Note also the direction: the failure is on the **consumer** side, where a broader declared type is the more capable one. The same signature with the payload flowing outwards would fail in the opposite direction, and identity would be over-constraining there too, just for the mirror-image reason. ## The rule worth carrying Every relation you state between two placeholders is a set of callers you have forbidden, and you owe each one a reason drawn from the body. Say identity when the code genuinely both produces and consumes that type; say acceptance when it only passes the value in; say nothing when the two were never related. Strength in a signature is not rigour — it is a bill the caller pays.
- Is loosening the relation between two placeholders a breaking change for existing callers?No. Weakening a relation only enlarges the set of accepted calls, so every call that compiled before still compiles. Tightening is the breaking direction. That asymmetry is why it is worth starting a dependent pair at the weakest relation the body needs: you can always strengthen it later behind a deliberate decision.
- How do you spot over-constraint before a caller complains?Walk each placeholder against the operations that mention it and write down the requirement each operation actually imposes. If the stated relation is stronger than the strongest requirement you found, the difference is a set of callers being refused for nothing. Reviewing the signature against the body, rather than against the first call site, is what catches it.
- When is identity between two placeholders genuinely the right relation?When the value travels both ways — the code both hands that type to the collaborator and takes the same type back, or stores it and later returns it to the caller under that name. Then anything weaker would let the two ends disagree, and the rejection of a broader partner is the constraint doing its job.
saying these in an interview costs you the question
- States the tightest relation available and treats the rejection as the caller's problem
- Fixes the rejection with a run-time conversion buried in the body
- Believes identity is the only way to keep two placeholders related at all
- Thinks widening a shared handler's declared payload for one route costs nothing elsewhere
- Assumes a signature that compiles for its author cannot be over-constrained
- Says loosening the relation would break the callers that already compile