skip to content

When a generic filter helper is called without an explicit type argument, what does the compiler use to fill its placeholder?

level: juniorimportance: must knowfreq 58%

answer

  1. a compile-time solve, not a guess
  2. evidence comes from the call site
  3. argument types set the floor
  4. the expected type sets the ceiling
  5. a declared bound caps the search

basics

~20 s

The compiler solves the placeholder from evidence at the call site: the static types of the arguments, the type the result is expected to have where the call sits, and any bound or default declared on the parameter itself.

solid answer

~40 s

Type inference is a compile-time solve, not a run-time guess. The compiler collects constraints from the call: each argument's **static** type must fit the position it is passed to, which says the placeholder is at least that wide, and the type the result is expected to have where the call sits says how wide it may be. A bound declared on the placeholder caps the search, and some languages allow a declared default. If the evidence pins exactly one type, the call compiles as though you had written the type argument yourself; if it pins none or several, you write it. Nothing about the objects at run time takes part: a basket declared as a basket of fruit infers fruit even when every element in it happens to be an apple.

code

pseudocode · 14 lines
pseudocode
function filterBasket<T>(basket: Basket of T, keep: Test of T) -> Basket of T
    result = new empty Basket of T
    for each item in basket
        if keep(item) then add item to result
    return result

# both arguments name the element type: the floor is fruit
ripe = filterBasket(fruitBasket, isRipe)

# only the basket names it: still enough
some = filterBasket(fruitBasket, acceptAnything())

# neither argument names it, and nothing declares the target
mystery = filterBasket(newEmptyBasket(), acceptAnything())

go deeper

for a junior

Recall that the placeholder is filled in at compile time from what the call site already knows: the declared types of the arguments and the type the result is expected to have.

for a middle

Explain the solve as constraint collection - arguments set a floor, the expected type sets a ceiling, a declared bound caps the candidates - and say what happens when the two directions conflict.

for a senior

Show that you read a confusing inference error the way the solver does, naming which argument supplied which constraint and which of the three evidence sources was missing.

for a principal

Judge signatures by how much evidence they hand the solver: a placeholder appearing only in the result position pushes an annotation onto every call site, and that cost lands on other teams.

## What inference is actually solving A generic declaration introduces a **placeholder**: a name standing for a type the declaration itself does not know. A filter helper over a shopping basket is declared once as *takes a basket of `T` and a test on `T`, returns a basket of `T`*, and that single body then serves baskets of anything. Before the body can be checked at a particular call, the placeholder has to become a real type. **Type inference** is the compile-time step that works out which type, from evidence already present at the call site, so the caller does not have to spell it out. The word *compile-time* carries most of the weight. Inference finishes before the program runs. It is constraint solving over the types written in the source; it never sees an object, a container's contents, or which branch was taken. ## The three sources of evidence 1. **The argument types.** Every argument expression has a static type. Passing it into a position declared as `T`, or as a container of `T`, produces a constraint: `T` must be wide enough to accept that type. A variable declared as a basket of fruit, passed into a `basket of T` position, says *`T` is at least fruit*. Arguments therefore push the answer **upward** from below. 2. **The expected type where the call sits.** Assigning the result to a variable declared as a basket of fruit says *whatever `T` becomes, a basket of `T` has to be usable as a basket of fruit*. Returning the call from a routine with a declared result type does the same. This evidence pushes the answer **downward** from above. 3. **The declaration itself.** A bound written on the placeholder caps the candidates, and the solve may not pick a type outside it. Some languages also let a declaration name a default type argument, which fills the placeholder when nothing else does. The solver gathers all of these and looks for the most specific type satisfying every one. Exactly one answer and the call compiles as if the type argument had been written by hand. No answer at all and the call is reported as a mismatch. Several equally good answers and the call is either reported as ambiguous or quietly widened, depending on the language's rules. ## Static types, not run-time values This is the point most first-screen answers miss, and the one an interviewer is usually checking. - A variable declared as a basket of fruit contributes **fruit**, whatever it currently holds. - A value created as a specific kind of fruit and stored in a fruit-typed variable contributes fruit, not the narrower kind. - An expression with no element in it contributes nothing useful, which is where inference begins to fail. - Lines *after* the call normally take no part: a solver looks at the call and the position it occupies, not at how the result is used three statements later. - A cast or an explicitly declared intermediate variable is therefore a way of *adding evidence*, not a way of fighting the solver. ## Reading a call the way the solver does | Call site | Evidence available | Placeholder solved as | |---|---|---| | Basket declared as fruit, test declared on fruit | Both arguments agree | Fruit | | Basket declared as fruit, test that accepts any item | The basket alone | Fruit | | Empty basket expression, test that accepts any item | None from the arguments | Unsolved: needs a target type, a default, or an explicit argument | | Empty basket expression, result assigned to a basket of fruit | The expected type | Fruit | | Basket declared as fruit, result assigned to a basket of sellable goods | Arguments from below, target from above | Fruit, and the wider target accepts it | The last row is worth reading twice: the two directions do not have to agree exactly, they only have to be compatible. Arguments set a floor, the expected type sets a ceiling, and any type between them is a candidate. ## Where inference stops and you take over Writing the type argument by hand is an ordinary part of the language, not an admission of defeat. It is the right move when: - nothing at the call site carries the type, so the solver has no floor and no ceiling; - several types fit equally well and the solver has no reason to prefer one; - the type that would be inferred is wider than you meant, and the wideness would only be discovered later; - the call is hard to read, and naming the type once is worth more than the brevity it costs. What inference is *not* is dynamic typing. The type is fixed and checked before the program runs; the only thing being saved is the typing of it in the source. That is why the answer to "what did it infer?" is always derivable from the call site alone, and why an inference error is best read by asking which of the three evidence sources was missing.

  • Does inference use the run-time class of an argument when it is more specific than the declared type?
    No. The solve happens before anything runs, so only the static type of each argument expression takes part. A variable declared as a basket of fruit contributes fruit even if every element in it is an apple. If you want the narrower answer, declare the variable narrower or write the type argument.
  • If the arguments already pin the placeholder, can the expected result type still change the answer?
    It can, where the argument evidence leaves room. Arguments constrain from below - the placeholder must be at least wide enough for them - and the expected type constrains from above, so a call whose result is assigned to a wider declared type can legitimately settle on the wider answer. When the floor and the ceiling are incompatible, the call is rejected instead.

saying these in an interview costs you the question

  • Says the compiler inspects the actual objects while the program runs
  • Thinks inference reads later lines that use the result
  • Believes an element's real class beats the declared type of the container
  • Assumes a type argument may never be written by hand
  • Claims a declared bound plays no part in the solve