An unrestricted placeholder type in a payroll totalling routine — what may its body do with each element?
answer
- no limit, almost no knowledge
- unstated is not unlimited
- the body sees the limit, not the argument
- pin at or below to call members
- violations surface at the call site
basics
~20 sOnly 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 sThe 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 linesfunction 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 sumgo deeper
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.
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.
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.
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