skip to content

A shared payroll totalling routine rejects a team's own pay-component type at compile time — how do you fix the limit?

level: seniorimportance: should knowfreq 40%

answer

  1. the message is at the call
  2. the defect is in the declaration
  3. limit should equal the call list
  4. widen admits callers, costs the body
  5. narrowing a published limit breaks callers

basics

~20 s

Read the rejection: it names the supertype the argument fails to satisfy. Then check what the body actually calls — usually the limit names a convenient concrete type, and renaming it to the smallest type carrying those operations admits the caller.

solid answer

~50 s

The message points at the call, but the defect is normally in the declaration. Start by listing the operations the body performs on the placeholder; then compare that list with the type the limit names. If the limit names a concrete class while the body only ever calls one or two operations declared higher up, the limit is over-tight and should be widened to the smallest type declaring exactly those operations — that admits the team's type and every future one like it, and the body still compiles. If the body genuinely needs operations only the named type declares, the limit is correct and the caller has to adapt instead, wrapping their value in a small type that satisfies the limit and delegates. What you must not do is loosen the limit and paper over the resulting gap with an unchecked conversion.

code

pseudocode · 13 lines
pseudocode
// Too tight: names a concrete type the body never needed
function totalOf<T at or below SalaryLine>(items: list of T) -> Amount
    var sum = zeroAmount
    for each item in items
        sum = sum + item.minorUnits()    // the only operation used
    return sum

// Right-sized: names the smallest type declaring minorUnits()
function totalOf<T at or below HasAmount>(items: list of T) -> Amount
    var sum = zeroAmount
    for each item in items
        sum = sum + item.minorUnits()
    return sum

go deeper

for a junior

The message names a supertype the supplied type does not sit under. Before changing anything, find out which operations the routine actually calls — that is what the limit is supposed to describe.

for a middle

Explain the two failure modes: a limit tighter than the call list rejects callers silently, a limit looser than it stops the routine compiling. Only the first one reaches other teams.

for a senior

Show the whole diagnosis: read the call list, right-size the limit, and choose between widening, wrapping at the caller, or splitting the routine. Say out loud why an unchecked conversion is not on the list.

for a principal

Own the compatibility rule for shared generic code: limits may be relaxed but not tightened once published, so the first publication should be sized to the call list rather than to whatever concrete type was handy.

## What the rejection is telling you A limit rejection is a **call-site** message: the supplied type argument does not sit at or below the type the limit names. That is all it says. It does not say the argument is wrong, and it does not say the routine is wrong — it says the two disagree, and it is your job to decide which side moves. The decisive evidence is not in the error and not in the caller. It is the body's **call list**: every operation the routine actually performs on a value of the placeholder type. A limit is only honest if it names the smallest type that declares that list. Anything tighter is excluding callers for operations nobody uses. ## Three fixes, and when each is right 1. **Widen the limit.** The body calls one or two operations that are declared on a small, widely implemented type, but the limit names a concrete class that happens to be where the first caller's type lived. Rename the limit to that smaller type. The team's type is admitted, every earlier caller is still admitted, and the body compiles unchanged because the operations it calls are still guaranteed. 2. **Keep the limit and adapt the caller.** The body genuinely calls operations only the named type declares — it does not just read an amount, it asks for a payroll period, a cost centre and a rounding rule. Widening would break the body. The caller instead wraps their value in a small type that satisfies the limit and delegates to what they already have. The cost is a wrapper and a layer of indirection, paid once per call site. 3. **Split the routine.** The body turns out to be two routines welded together: a totalling core that needs one operation and a formatting tail that needs many. Extract the core with the smaller limit, leave the tail with the larger one, and most callers only ever touch the core. What is never a fix is loosening the limit and then forcing the value back with an unchecked conversion inside the body. That trades a compile-time rejection — precise, at the call, with the failing type named — for a run-time failure at an arbitrary later point. ## Choosing the limit from the body - Enumerate the operations the body performs on the placeholder; that list, not the type of the first argument you had in mind, is the requirement. - Find the smallest type that declares all of them, and name that. - If the list is empty, drop the limit entirely — the value is only being carried. - If the list is long and heterogeneous, treat that as a signal about the routine's cohesion before treating it as a signal about the limit. ## The cost of each mistake | The limit is | The body | Callers | The symptom | |---|---|---|---| | Tighter than the call list | Compiles, uses less than it is guaranteed | Legitimate types are refused | Call-site rejections from teams whose types would have worked | | Exactly the call list | Compiles, uses exactly what it is guaranteed | Everyone who can do the work is admitted | None | | Looser than the call list | Fails to compile at the declaration | Irrelevant — nothing ships | An error in the routine itself, before any caller exists | The third row is the reassuring one: making a limit too loose is self-detecting, because the body stops compiling. Making it too tight is silent in the routine and only shows up as somebody else's rejected call, which is why over-tight limits survive in shared code for a long time. ## Changing a limit that is already published Direction matters here, and it is easy to state backwards: - **Widening** the limit — naming a more general type — admits every argument the tighter limit admitted, plus more. No existing call site breaks. The price is paid inside the routine, which now has fewer guaranteed operations to spend. - **Narrowing** the limit — naming a more specific type — is the breaking direction. Arguments that satisfied the old limit may not satisfy the new one, and those call sites stop compiling. So a shared routine can safely relax its limit as it learns what it really needs, and cannot safely tighten it once others depend on it. A useful habit when first publishing a generic routine is therefore to start from the call list rather than from a convenient concrete type, because the tighter you start, the less room you have to move. ## What the rejection is not Finally, be clear about what has *not* happened. No value has been examined, nothing has run, and there is no risk that the code would have worked at run time if only the check had been skipped. The limit describes the operations the body is allowed to spend, and the rejection means the argument does not supply them.

  • Is widening a published limit safe for existing callers?
    Yes for callers: every argument that satisfied the tighter limit still sits under the wider one, so no call site breaks. The routine pays instead, because it can now call only what the more general type declares. Narrowing is the breaking direction and should be treated as an incompatible change.
  • When is keeping the tight limit the right answer?
    When the body genuinely spends operations that only the named type declares. Widening then breaks the routine itself, which is worse than rejecting one caller. The caller adapts by wrapping their value in a small type that satisfies the limit and delegates to what they already have.
  • Why do over-tight limits survive so long in shared code?
    Because they are invisible from inside. The routine compiles, its tests pass, and the only symptom is a rejection in somebody else's build. An over-loose limit, by contrast, stops the routine compiling immediately, so it never ships.

saying these in an interview costs you the question

  • Blames the caller's type without reading what the body calls
  • Suggests loosening the limit and forcing the value with an unchecked conversion
  • Thinks narrowing a published limit is a compatible change
  • Widens the limit to the widest type and is surprised the body stops compiling
  • Reads the rejection as evidence the code would fail at run time