skip to content

How does the trailing-lambda syntax interact with overload resolution and SAM conversion? When can it cause an ambiguity or surprising call?

level: seniorimportance: should knowfreq 30%

answer

  1. Trailing lambda doesn't change resolution rules
  2. Two function/SAM overloads → ambiguity
  3. SAM conversion fits a lambda to Runnable/Comparator
  4. Disambiguate: SAM constructor or named arg
  5. fun interface lambdas are SAM-convertible since 1.4

basics

~20 s

The trailing lambda is just normal sugar, so the compiler still matches it to a function-type parameter. Problems appear when two overloads both accept a final function-type argument — then the call can be ambiguous.

solid answer

~50 s

A trailing lambda is resolved exactly like a parenthesized last argument: the compiler picks the overload whose final parameter is a compatible function type. Ambiguity arises when **two overloads** each end in a function-type parameter that the lambda could satisfy — e.g. one taking `() -> Unit` and another taking a Java SAM interface like `Runnable`. With Java interop, Kotlin performs **SAM conversion**, turning a lambda into an instance of a single-abstract-method interface, so a lambda can match both a Kotlin `() -> Unit` and a Java `Runnable`, producing an overload-resolution error you must disambiguate (cast, named argument, or wrap explicitly: `Runnable { ... }`). Inside the lambda, the inferred parameter and return types depend on which overload wins, so an ambiguous or unexpectedly-chosen overload can change `it`’s type. Trailing-lambda sugar never changes resolution rules — it only changes where the braces sit.

code

kotlin · 8 lines
kotlin
fun interface Handler { fun handle(x: Int) }

fun on(h: Handler) {}
fun on(h: (Int) -> Unit) {}

// on { println(it) }  // ambiguous
on(Handler { println(it) })   // pick the fun interface
on(h = { x -> println(x) })   // pick the function type

go deeper

for a junior

Understands the lambda must match the last parameter but won't anticipate overload ambiguity.

for a middle

Recognizes two function-type overloads can clash and that you must disambiguate somehow.

for a senior

Explains SAM conversion, fun interface, and concrete fixes (SAM constructor, named argument).

for a principal

Advises against ambiguous overload designs and reasons about source/binary-compatible API evolution.

## Resolution is unchanged by the sugar Moving the lambda outside the parentheses does **not** alter overload resolution. The compiler still treats it as the value for the last parameter and selects an applicable overload by parameter types. ## Where ambiguity comes from Problems appear when multiple overloads end in a function-typed parameter that the same lambda could fit. ```kotlin fun schedule(task: () -> Unit) {} fun schedule(task: Runnable) {} // Java SAM type schedule { println("run") } // AMBIGUOUS ``` Both are applicable because of **SAM conversion** — Kotlin can convert a lambda into a **S**ingle-**A**bstract-**M**ethod interface instance (here `Runnable`). The lambda satisfies the Kotlin function type *and* the SAM type, so resolution is ambiguous. ### Disambiguating ```kotlin schedule(Runnable { println("run") }) // explicit SAM constructor schedule(task = { println("run") }) // pins the () -> Unit overload via name ``` Note: for **Kotlin-declared** `fun interface` types, since Kotlin 1.4 a lambda is also SAM-convertible, so the same ambiguity can occur even without Java. ## The lambda's inferred types follow the chosen overload The parameter and return types inside the braces are inferred from the matched parameter's function type. If two overloads differ — say `map(f: (T) -> R)` vs a hypothetical `map(f: (T, Int) -> R)` — the arity of your lambda steers selection, and `it` only exists when the single-parameter form is chosen. ## Practical guidance - Avoid two overloads that both end in a function/ SAM parameter; it forces callers to disambiguate. - Prefer distinct names or distinct non-lambda parameters. - When consuming such an API, reach for the **SAM constructor** (`Runnable { }`, `Comparator { a, b -> ... }`) or a **named argument** to make intent explicit. ```kotlin val c = Comparator<Int> { a, b -> a - b } // SAM constructor list.sortedWith(Comparator { a, b -> a - b }) ```

  • What is SAM conversion?
    Automatic conversion of a lambda into an instance of a single-abstract-method interface (e.g. Runnable, Comparator, or a Kotlin `fun interface`), so you can pass a lambda where an interface is expected.
  • Does moving the lambda outside the parentheses ever change which overload is chosen?
    No. The braces' position is purely syntactic. Overload selection uses the same applicability and specificity rules either way.

saying these in an interview costs you the question

  • Thinks trailing-lambda placement affects overload resolution
  • Unaware of SAM conversion or that fun interface enables it
  • Cannot name a disambiguation technique
  • Assumes a lambda can only match a Kotlin function type, never a SAM interface

context