Consuming services keep hitting rejected arguments at a shared policy library's invariant signatures - where does the fix belong?
answer
- the error lands where it cannot be fixed
- the signature owns the direction
- narrow one occurrence, or declare once
- split a one-directional type out
- copies and conversions defer the cost
basics
~20 sWith the library, not the services. The direction belongs to the signature or the declaration, and a caller cannot state it from outside. Narrowing each occurrence fixes one signature; declaring the direction fixes all of them, where the type allows it.
solid answer
~50 sThe symptom is misleading: the error appears in every consuming service, but none of them owns the thing that is wrong. A parameter's type either permits an argument over a different type argument or it does not, and only the signature - or the declaration behind it - can say so. The library has three real fixes. It can narrow the occurrence in each signature that hits the problem, which is surgical and reversible but must be repeated and can be forgotten in the next signature. It can write the direction on the type parameter once, which fixes every signature present and future, but only if the parameter sits in one direction across all members and the promise is acceptable forever. Or it can publish a one-directional supertype and keep the full type invariant beneath it. What the services reach for instead - a copy or an unchecked conversion - defers the cost rather than removing it.
code
pseudocode · 15 linestype Evaluator<R> {
decide(request: R): Decision // R only in an input position
}
// the symptom: this signature is invariant in R
function install(e: Evaluator<AdminRequest>)
install(anyRequestEvaluator) // rejected, though it handles strictly more
// fix at the use site: this one signature accepts a broader evaluator
function install(e: Evaluator<consumes AdminRequest>)
// fix at the declaration: every signature over Evaluator, now and later
type Evaluator<consumes R> {
decide(request: R): Decision
}go deeper
Recall the ownership rule: the direction is part of the parameter's type in the signature, so only the code that owns that signature can change it.
Explain why narrowing one occurrence does not generalise - the declaration stays invariant, so the next signature over the same type begins from invariance again.
Diagnose from the workarounds: copies, unchecked conversions and widened domain types appearing across several consumers all point at one missing direction in the library.
Decide by frequency and reach: one affected signature is a narrowing, most signatures affected is a declaration or a split, and unrecompilable consumers change which of those is safe.
## Reading the symptom correctly A shared library publishes a signature that takes a policy evaluator at one request type. A service holds an evaluator written against a broader request type - one that handles strictly more than the signature asks for - and the compiler rejects it. An evaluator only ever takes a request as **input**, so the parameter sits in a consuming position and the safe substitution genuinely runs from the broader evaluator to the narrower need. The rejection is not protecting anyone; it is the default invariance of a type nobody has said anything about. The error lands in the service, and this is what sends teams down the wrong path. The service can see the failure and cannot repair its cause: the direction is a property of the parameter's type as written in the library's signature, and no caller can restate it from the call site. **The error is reported where it is observed, not where it is owned.** ## The three fixes, and what each one costs 1. **Narrow the occurrence, in that signature.** The library edits one parameter to take a consuming view of the evaluator. It is surgical, it promises nothing about the type, and it can be undone. Its costs are repetition and silence: the next signature over the same type starts invariant again, and nothing in the library's own build will notice, because a missing narrowing is not an error anywhere - it is only an argument someone could not pass. 2. **Declare the direction on the type parameter.** One edit, checked once against every member, and every signature over that type - including the ones not yet written - accepts the broader evaluator. This is the fix that stops the problem recurring. It is available only if the parameter appears in one direction across the whole type, and it is a published promise: afterwards the type may never gain a member using the parameter in the other position. 3. **Split a one-directional type out.** Publish a smaller type that only consumes (or only produces) and can therefore carry the direction, and let the full type remain invariant beneath it. Signatures then take the smaller type. This costs an extra name in the public surface and a migration of existing signatures, and it buys the free substitution without committing the full type to anything. ## What the services will do if nobody fixes it Left alone, consumers find their own way past a compiler error, and every route is worse than the fix: - **copy into the exact type** - allocation on a path that did not need it, plus a snapshot that can drift from the original if anything mutates afterwards; - **an unchecked conversion** - the compile-time rejection becomes a possible run-time failure, in a service the library author will never see; - **widen the service's own type to match the signature** - precision is lost everywhere upstream of the call, so one library's missing narrowing propagates into a service's own domain types; - **duplicate the evaluator** - a second implementation written only to satisfy a parameter type, and now two things to keep in step. All four are visible in review as workarounds. Their presence in several services at once is the strongest signal that the library, not the services, has the defect. ## Choosing between the fixes | situation | the fix that fits | |---|---| | the type is genuinely one-directional | declare the direction once | | the type reads and writes, one signature affected | narrow that occurrence | | the type reads and writes, most signatures affected | split a one-directional supertype out | | consumers cannot be recompiled together | prefer the narrowing, which changes no published promise | The judgment is about how often the flexibility will be wanted. One hot signature is a narrowing. Every signature wanting the same view is a declaration waiting to be written, or a type that should have been two types. ## The diagnostic contrast worth saying out loud With the direction on the declaration, a mistake is **one error, in the author's file, before publication** - the compiler checks the promise against every member at that point. With narrowing, the declaration always compiles, every signature always compiles, and the mistakes are distributed across consumers as rejected arguments, discovered one team at a time, in an order nobody controls. That difference in where failures surface is usually the real reason a library owner reaches for the declaration when the type permits it - not the keystrokes saved.
- The service ships an unchecked conversion to get past the rejection. What has it actually traded away?A compile-time rejection for a failure that can only appear at run time, in production, in the service rather than the library. It also hides the library's defect: once the workaround compiles, nobody reports the signature, and the next signature repeats the mistake with nobody watching.
- Why does adding the narrowing to one signature not stop the problem recurring?Because a narrowing is a property of that occurrence only. The declaration is still invariant, so the next signature written over the same type starts from invariance again, and its author has to remember something the compiler will never remind them of.
- When is the split into a one-directional type better than narrowing every signature?When most signatures want the same view. At that point the repetition is telling you the public surface is wrong: a type that only consumes, or only produces, can carry the direction on its own declaration, and signatures that take it inherit the substitution without anyone restating it.
saying these in an interview costs you the question
- Tells the calling service to cast the argument and move on
- Thinks a consuming service can narrow the library's parameter itself
- Treats copying into the exact type as a free workaround
- Assumes every invariant signature can be fixed at the declaration
- Blames the services for holding a broader evaluator