skip to content

Why can a generic call that compiles on its own line fail when nested directly inside another generic call?

level: middleimportance: should knowfreq 44%

answer

  1. the inner call has no target
  2. a declared variable supplies one
  3. the surrounding position is itself a placeholder
  4. solve order decides the outcome
  5. pin one side, the rest follows

basics

~20 s

On its own line the call takes an expected type from the variable's declaration. Nested as an argument it has none, because the surrounding parameter is itself a placeholder, so two unknowns have to be solved together and neither side supplies the other.

solid answer

~50 s

A declared variable is evidence. Writing `basket: Basket of Fruit = makeEmptyBasket()` hands the inner call a concrete expected type, so its placeholder is solved before anything else needs it. Nest the same call as an argument to another generic call and that evidence disappears: the position it now occupies is declared in terms of the *outer* call's placeholder, which is itself unknown. The solver has two unknowns and one equation. Whether that works depends on solve order - some languages push the expected type inward, so the outer call's own target can settle both; others solve the inner call first, and with nothing to go on it fails or widens. The fixes all amount to injecting one concrete type: split the statement and declare the intermediate, or write the type argument on whichever call has no other evidence.

code

pseudocode · 14 lines
pseudocode
function newEmptyBasket<T>() -> Basket of T
function acceptAnything<T>() -> Test of T
function filterBasket<T>(basket: Basket of T, keep: Test of T) -> Basket of T

# split: the declaration pins the inner call, the outer reads a known type
start: Basket of Fruit = newEmptyBasket()
ripe = filterBasket(start, acceptAnything())

# nested: no argument carries a concrete type, and the position
# the inner call occupies is declared in terms of the outer placeholder
ripe = filterBasket(newEmptyBasket(), acceptAnything())

# same expression, one type written by hand
ripe = filterBasket(newEmptyBasket<Fruit>(), acceptAnything())

go deeper

for a junior

Remember the practical rule: when a nested generic call will not compile, pull it out into its own statement with a declared type, and the same code usually compiles unchanged.

for a middle

Explain why the declaration is evidence - it gives the inner call a concrete expected type that the nested position, declared in terms of another placeholder, cannot supply.

for a senior

Diagnose the expression rather than the message: walk each call asking which concrete type touches it, annotate the one where the answer is none, and reject widening a parameter as the fix.

for a principal

Weigh how much a published signature depends on the caller's solve order; a helper whose placeholder is only reachable from the result position makes every nested use of it a support question.

## Two calls, one unknown each Take a helper that builds an empty basket, declared as *returns a basket of `T`*, and a filter helper declared as *takes a basket of `T` and a test on `T`, returns a basket of `T`*. Each is fine alone. Nest one inside the other and you get a call whose every part is waiting on some other part: the outer helper's placeholder is supposed to be read off its argument, and the argument is a call whose own placeholder is supposed to be read off the position it sits in. Neither has a concrete type to offer, and the solver is left with two unknowns and nothing to anchor them. The reason the split version works is not that it is simpler. It is that the declaration on the intermediate variable is **evidence the nested form does not contain**. ## What a declared variable actually contributes - It gives the inner call an **expected type** - a concrete ceiling that immediately pins the inner placeholder. - It makes the intermediate's type a **fact** rather than a question, so the outer call now reads an ordinary typed argument. - It does so at compile time only: in the normal case the same calls run, in the same order, with the same results. This is why the temporary variable is not a workaround. It is a type argument written in a place readers already look, plus a name for what flows through. ## Inside-out versus outside-in Languages differ in how far they push the expected type through an expression, and that difference is exactly what decides whether a nested call compiles. | Solve strategy | What the inner call sees | Consequence | |---|---|---| | Inside-out: solve each sub-expression on its own, then check it fits | No expected type at all | The inner placeholder fails, or defaults to its bound and widens the whole expression | | Outside-in: push the expected type of the whole expression into its parts | The outer call's target, propagated inward | The nested form often compiles, and the failure moves to expressions with no target either | A candidate does not need to know which language does which. What they should be able to say is *what the strategy determines*: whether an unknown in one position can be solved from a known in another, and how far that knowledge travels. ## Three ways to break the deadlock 1. **Split the statement and declare the intermediate.** Best when the intermediate deserves a name anyway, which is most of the time. 2. **Write the type argument on the call that has no other evidence.** Usually the inner one: once its result type is concrete, the outer call is solved from an ordinary argument. Writing it on the outer call only helps when the outer placeholder is what would constrain the inner, which is the outside-in case. 3. **Give the expression a target.** Declaring the type of the variable the *whole* nested expression lands in sometimes suffices, because the ceiling then has a path inward. Where it does not, the error message says so and you fall back to option 2. What is not a fix: widening a parameter to the universal supertype so no placeholder remains. That removes the checking the placeholder existed for and pushes narrowing onto every caller. ## Reading the error Inference errors on nested calls are notoriously hard to read because the message names a placeholder the programmer never wrote and reports it at a position that looks fine. The habit worth building is to stop reading the message and instead ask, for each call in the expression, **what concrete type touches it**. If the honest answer for some call is "none", that call is the one to annotate - and the answer to the question of whether the nesting is worth keeping usually follows immediately. ## Why this is the standard follow-up An interviewer who has already asked about an empty container reaches for this one next, because it separates two models. The weaker model is "the compiler works out types from the code", which predicts that splitting a statement changes nothing. The stronger model is "the compiler solves constraints, and a declaration is a constraint", which predicts exactly what happens: the split version compiles, the nested version does not, the program behaves identically either way, and one hand-written type argument replaces the variable when you want the expression to stay in one piece.

  • Does splitting the call into two statements change what the compiled program does?
    In the normal case, no - the same calls happen in the same order and produce the same values. The declaration adds a compile-time constraint, not work. It is a type argument written where readers already look, with the bonus that the intermediate gets a name.
  • If you keep the call nested, which of the two calls should carry the explicit type argument?
    The one with no other evidence, which is usually the inner call: once its result type is concrete, the outer call is solved from an ordinary typed argument. Annotating the outer call helps only where the language pushes the expected type inward, because then the outer placeholder is what would have constrained the inner one.
  • Why do inference errors on nested calls read so badly?
    The message names a placeholder the programmer never wrote, and reports it at a position that looks perfectly ordinary, because the failure is a property of the whole expression rather than of one sub-expression. The reliable technique is to ignore the wording and ask, call by call, which concrete type touches it.

saying these in an interview costs you the question

  • Says nesting generic calls is forbidden by a syntax rule
  • Thinks the inner call is always solved first and independently
  • Believes splitting the statement changes what the program does at run time
  • Assumes only the outer call may carry an explicit type argument
  • Dismisses the intermediate variable as a workaround carrying no type meaning
  • Suggests widening a parameter to the universal supertype to make it compile