skip to content

Constraints and Bounds

Narrowing a placeholder so the code can call something on it: limits in both directions, several at once, and self-referential ones. An unrestricted placeholder can only be moved around.

on this pageshow

questions

16

An unrestricted placeholder type in a payroll totalling routine — what may its body do with each element?

level: juniorimportance: must knowfreq 66%

answer

  1. no limit, almost no knowledge
  2. unstated is not unlimited
  3. the body sees the limit, not the argument
  4. pin at or below to call members
  5. violations surface at the call site

basics

~20 s

Only what every type supports: hold it, pass it on, return it. With no limit written, the placeholder is implicitly pinned at the widest type available, so no pay-component operation such as reading an amount is callable on it.

solid answer

~40 s

The body is compiled once, against what the declaration promises about the placeholder — not against whatever type a caller later supplies. An unrestricted placeholder promises almost nothing: the value can be stored, put into a collection typed by the same placeholder, handed to another routine that also takes it, and returned, but nothing domain-specific can be called on it. To total pay components the body needs to call an amount operation, so the placeholder has to be pinned **at or below** the pay-component supertype. That limit buys two things at once: the body gains every operation that supertype declares, and the call site is checked so only a type carrying those operations can be supplied. The routine stays generic — it is still one body over every subtype, including ones written later.

code

pseudocode · 11 lines
pseudocode
function totalOf<T>(items: list of T) -> Amount
    var sum = zeroAmount
    for each item in items
        sum = sum + item.minorUnits()   // rejected: T is only known to be the widest type
    return sum

function totalOf<T at or below PayComponent>(items: list of T) -> Amount
    var sum = zeroAmount
    for each item in items
        sum = sum + item.minorUnits()   // accepted: every T declares minorUnits()
    return sum

go deeper

for a junior

Remember the trade: an unrestricted placeholder can only be held, passed and returned. Naming a supertype it must sit at or below is what unlocks calling that supertype's operations inside the body.

for a middle

Explain the mechanics: the body is compiled once against the declared limit, each call site is checked against it separately, and nothing is deferred to run time. Then explain why that ordering decides where errors appear.

for a senior

Show that you pick limits from the body's actual call list rather than from whatever type is handy, and that you notice when a convenient concrete limit is quietly excluding callers who would work fine.

for a principal

The angle worth owning is the standard: teams that default to the loosest limit push failures outward, teams that default to the tightest freeze their own APIs. Set a house rule tying the limit to the operations the body spends.

## What "unrestricted" actually means A **placeholder type parameter** is a name a routine uses for a type it does not know when it is written. The key rule is that the body is type-checked **once**, against the promise the declaration makes about that placeholder, and never re-checked against the individual types callers supply. So the promise is the entire budget of operations the body may spend. With nothing written, the promise is the widest one the type system has. Type systems differ here: where there is a single root type that every type sits under, an unrestricted placeholder is implicitly pinned **at or below that root**; where there is no such root, an unrestricted placeholder carries no operations at all. Either way the useful conclusion is the same — *unstated does not mean unlimited*. There is always an effective limit, and leaving it out picks the loosest one. ## What the body may do with such a value With the widest limit in force, a value of the placeholder type can be: - assigned to a local or a field declared with that same placeholder; - put into, and taken out of, a collection typed by that placeholder; - passed to another routine that also accepts an unrestricted placeholder; - returned, which is what preserves the caller's own type through the call; - acted on by whatever operations the root type itself declares, which differs between type systems. What it may **not** do is anything the domain cares about: read a pay amount, compare two values for ordering, format a payslip line. Those members belong to a type the declaration never named, so the compiler has no basis for allowing the call. ## Adding an at-or-below limit Pin the placeholder at or below the pay-component supertype and the routine can total. The declaration now says: *whatever type the caller supplies, it is a pay component or something more specific*. Because every admissible argument declares the amount operation, the body may call it. Notice what is **not** lost. The routine is still generic. A list of bonuses still enters as a list of bonuses, and the placeholder still stands for that specific type in the signature, so a result that mentions the placeholder comes back at the caller's own type rather than flattened to the supertype. The limit narrows *which* types may be supplied; it does not collapse the type that is supplied. ## Where the check happens 1. **At the declaration.** The body is checked against the limit alone. If the body calls something the named supertype does not declare, the routine itself fails to compile — no caller needed. 2. **At the call site.** Each supplied type argument is checked against the limit. A type that does not sit at or below the named supertype is rejected there, and the rejection names the supertype it failed to satisfy. 3. **At run time.** Nothing. The limit has already done its work; there is no per-value test and no failure deferred to execution. That ordering explains a common surprise: the error a caller sees points at the call, not at the member call deep inside the body, because the body was never in doubt. ## How tight the limit should be The honest rule is: **name the smallest type that declares every operation the body actually calls.** Naming something bigger and more convenient — a concrete class the team happens to have — locks out callers for operations the body never uses. | Declaration | Body may call | Callers admitted | |---|---|---| | Unrestricted placeholder | Only what the root type declares | Every type | | Placeholder at or below a named supertype | Everything that supertype declares | That type and everything below it | | A concrete parameter type, no placeholder at all | Everything that type declares | That type and everything below it, but the argument's own specific type is no longer tracked in the signature | The third row is worth dwelling on, because it is the alternative juniors reach for first. Declaring the parameter as the supertype directly does grant the same operations — but the routine can no longer say anything about the relationship between what went in and what comes out, because there is no placeholder left to carry that relationship. ## The confusion to avoid The frequent mistake is to imagine the body being re-checked for each caller: "the argument is a bonus, so the bonus operations are available." They are not. The body saw only the limit. Whatever the caller knows about its own type stays on the caller's side of the boundary, which is exactly what makes one compiled body safe for every argument.

  • If no limit is written at all, what is the placeholder effectively limited to?
    To the widest thing the type system offers. Where a single root type exists, the placeholder is implicitly pinned at or below it, so only the root's own operations are available; where there is no root, an unrestricted placeholder supports no operations at all. The limit is still there, just at its loosest setting.
  • Does pinning the placeholder at or below a supertype make the routine less reusable?
    It narrows who may call, not how widely the body is written. Every type at or below the named supertype still works, including ones written years later. Reuse is only genuinely lost when the limit is tighter than the operations the body actually uses — then it excludes callers for nothing.
  • Why does the rejection appear at the call site rather than inside the body?
    Because the body was checked once, against the limit, and passed. The only thing left to check per caller is whether the supplied type argument sits at or below that limit, and that check lives at the call. The message therefore names the supertype the argument fails to satisfy.

An unrestricted placeholder is a sealed parcel: you can carry it, shelve it and hand it on, but you cannot use what is inside. Naming a limit is agreeing, in writing, what every such parcel contains.

saying these in an interview costs you the question

  • Claims an unrestricted placeholder can call any member of the supplied type
  • Says the limit is verified at run time when the value arrives
  • Believes an at-or-below limit excludes subtypes written after the routine
  • Treats the limit as documentation rather than something the compiler enforces
  • Assumes that writing no limit means the placeholder has no limit
open as a page

A board routine needs its departure placeholder both time-orderable and fixed-width renderable - why two requirements instead of one supertype?

level: middleimportance: must knowfreq 58%

basics

~20 s

Listing two requirements on one placeholder admits any type that already orders and already renders, unchanged. A combined supertype forces every candidate to be redeclared under it, which is impossible for types you do not own, and welds two unrelated capabilities together.

open as a page

In a tournament ladder, why limit the entrant placeholder in terms of itself rather than to a plain comparable supertype?

level: middleimportance: must knowfreq 58%

basics

~20 s

A self-referential limit types each entrant's comparison against its own type, so comparing two unrelated kinds of entrant fails to compile. A plain comparable supertype only promises comparison with some entrant, leaving the mismatch to surface at run time.

open as a page

Requiring an add-in to implement a published interface versus requiring only the operations you call — what does each demand of the add-in team?

level: middleimportance: must knowfreq 62%

basics

~20 s

Naming a supertype demands that the add-in team edit their type's declaration and take a build dependency on your published artifact. Requiring a shape demands only that the members exist, so a type written before your host can qualify unchanged.

open as a page

What decides whether a payroll routine's placeholder is pinned at or above a type rather than at or below it?

level: middleimportance: must knowfreq 54%

basics

~20 s

What the body does with the value. If it calls that type's own operations, pin the placeholder at or below the type; if it only hands values of that type in somewhere, pin it at or above.

open as a page

Why may at most one of a placeholder's several requirements be a state-carrying type, where a language has single implementation inheritance?

level: middleimportance: should knowfreq 41%

basics

~20 s

Because the combination must be satisfiable: some real type has to descend from every listed requirement at once. Under single implementation inheritance no type can descend from two state-carrying types, so a second one would make the requirement list unsatisfiable by construction.

open as a page

How does a self-referential limit on a base builder stop a chained configuration step from degrading to the base type?

level: middleimportance: should knowfreq 46%

basics

~20 s

The base declares a placeholder standing for the most-derived type, limits it to types deriving from the base at that placeholder, and returns the placeholder from every chained step - so each call hands back the derived type and later derived-only steps still resolve.

open as a page

When a generic host rejects a candidate type, how does a missing-member failure differ from a missing-supertype failure in what it tells the author?

level: middleimportance: should knowfreq 46%

basics

~20 s

A missing supertype is one fact — the declaration is absent — so the report names the type to implement and cannot say which operation was wanted. A missing member is a diagnosis: the checker names the operation and the signature that failed.

open as a page

When a placeholder carries several requirements, what type do you write to store or pass on a value satisfying all of them?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Usually none: the combination is anonymous and exists only in the requirement list. You keep both capabilities by giving the receiving routine its own placeholder with the same requirements; storing the value in a field normally forces a named type that carries both.

open as a page

When two entrants derive from a base that closed the self-referential comparison, what mismatch does that limit still allow?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Siblings still compare freely. The limit is satisfied once, at the level where the recursion was closed, and every type below that level inherits a comparison typed against the closing type - so two unrelated subtypes of it pair up with no complaint.

open as a page

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%

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.

open as a page

A departure placeholder in a shared library has grown from two requirements to a proposed third - how do you decide between accepting it, splitting the routine, or naming a concept?

level: principalimportance: should knowfreq 27%

basics

~20 s

Decide from the body and the callers, not the list length. Split when only one branch needs the third capability, name a concept when the three always travel together and you own the candidate types, and otherwise take the capability as a parameter rather than demanding it from the type.

open as a page

For a base type other teams extend, when is a self-referential parameter worth the cost it pushes onto every subtype?

level: principalimportance: should knowfreq 28%

basics

~20 s

Spend it only when an operation on the base must genuinely hand back or accept the most-derived type and the hierarchy is extended by people you will not review. Everywhere else the plain limit reads once, admits more types and gives up nothing callers relied on.

open as a page

You set the extension contract for add-in teams that cannot depend on your code — do you require a published supertype or a shape?

level: principalimportance: should knowfreq 40%

basics

~20 s

Decide by what the contract carries. Behaviour, ordering and a need to enumerate and notify conformers argue for a published supertype with host-supplied adapters; reach across teams who cannot take the dependency argues for a shape, kept narrow, plus published conformance tests.

open as a page

What may a payroll routine get back out of a destination whose element type is pinned at or above the ledger-line type?

level: middleimportance: nice to knowfreq 31%

basics

~20 s

Only what the widest type guarantees. Pinning at or above a named type promises every acceptable value is at least that general, so anything read back is some unknown supertype — the ledger line's own operations are not available on it.

open as a page

A type satisfies every operation your shape-based requirement names yet means something entirely different — what risk has the requirement failed to catch?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

Accidental conformance: a shape checks member names and signatures, never meaning, so a type that never intended to be an add-in can match and be accepted. The mismatch compiles clean and surfaces as wrong behaviour at run time.

open as a page