skip to content

Inside `table { row { } }`, how does the compiler know that `row` belongs to `Table`? Explain how the receiver lambda resolves calls.

level: middleimportance: must knowfreq 50%

answer

  1. Lambda with receiver = hidden `this`
  2. Unqualified call resolves against implicit receivers, innermost first
  3. Nesting switches the implicit receiver
  4. Outer receivers stay in scope (callable)
  5. Disambiguate with this@table / this@row

basics

~10 s

The block passed to table runs with a Table as its this. So an unqualified call like row is looked up as a member of Table, exactly as if you wrote thisTable.row.

solid answer

~40 s

`table` takes `init: Table.() -> Unit`, a lambda with receiver. When `apply` invokes it, the new `Table` becomes the lambda's **implicit receiver** (`this`). Unqualified names inside the block resolve against the receiver's scope first, so `row { }` binds to `Table.row`. Nesting works because `row` itself takes `Row.() -> Unit`: inside `row { }` the implicit receiver switches to a `Row`, so calls there resolve against `Row`. At any point the innermost receiver wins, but outer receivers remain reachable (you can still touch the enclosing `Table` via its members or a labeled `this@table`). This is pure compile-time name resolution — no reflection. It's why every nested call is statically type-checked.

code

kotlin · 6 lines
kotlin
table {            // this: Table
    row {          // this: Row
        cell("a")  // Row.cell
        this@table // the enclosing Table receiver
    }
}

go deeper

for a junior

Knows the block runs with this set to the object so row means Table.row.

for a middle

Explains implicit-receiver resolution, innermost-wins, and receiver switching on nesting.

for a senior

Adds labeled this@, the multiple-receivers-in-scope subtlety, and that it's all compile-time.

for a principal

Connects scope/receiver shadowing to DSL safety pitfalls and why @DslMarker exists to constrain outer-receiver access.

## Receivers and scopes A **lambda with receiver** `Table.() -> Unit` is compiled like an extension function: the receiver is passed as a hidden first argument and is available as the implicit `this` inside the body. When you write: ```kotlin fun table(init: Table.() -> Unit): Table = Table().apply(init) ``` `apply` calls `init` with the freshly-created `Table` as the receiver. Inside the `{ ... }` you passed, that `Table` is `this`. ## Name resolution: implicit receivers Kotlin resolves an unqualified call (like `row`) by searching the **implicit receivers** in scope, innermost first: 1. The lambda's own receiver (`this`, the `Table`). 2. Enclosing receivers (outer lambdas, the surrounding class). 3. Top-level / imported declarations. So `row { }` is found as `Table.row` because the `Table` receiver is in scope. There is **no runtime lookup** — this is ordinary compile-time overload resolution, which is exactly why it is type-safe. ## Nesting and receiver switching ```kotlin class Table { fun row(init: Row.() -> Unit) { /* ... */ Row().apply(init) } } class Row { fun cell(text: String) { /* ... */ } } table { // this == Table row { // this == Row (innermost receiver wins) cell("a") // resolves to Row.cell // header // would resolve to Table.header (outer receiver) if it existed } } ``` Inside `row { }`, the implicit receiver becomes the `Row`. The outer `Table` is still an implicit receiver, so its members remain callable — both `Row` and `Table` are in scope simultaneously. ## Disambiguating with labeled `this` When receivers overlap you can name one explicitly: ```kotlin table { row { [email protected]() [email protected]("x") } } ``` `this@table` refers to the `Table` receiver, `this@row` to the `Row` receiver. The label is the name of the function whose receiver you mean. ## Why this matters Because resolution is static, an undefined `cell` or a `cell(123)` (wrong arg type) fails to compile. The same correctness you get from normal method calls applies inside the DSL.

  • If both `Table` and `Row` declare a method `id()`, which one does an unqualified `id()` inside `row { }` call?
    `Row.id()` — the innermost implicit receiver wins. Use `[email protected]()` to call the outer one.
  • Does this resolution use reflection at runtime?
    No. It is compile-time overload/name resolution; the receiver is just a hidden parameter, so there's no runtime cost beyond a normal call.

saying these in an interview costs you the question

  • Claiming the receiver is resolved dynamically/by reflection
  • Thinking only the innermost receiver is reachable (outer ones are also in scope)
  • Not knowing about labeled `this@name`
  • Believing nested calls aren't type-checked

context