Inside `table { row { } }`, how does the compiler know that `row` belongs to `Table`? Explain how the receiver lambda resolves calls.
answer
- Lambda with receiver = hidden `this`
- Unqualified call resolves against implicit receivers, innermost first
- Nesting switches the implicit receiver
- Outer receivers stay in scope (callable)
- Disambiguate with this@table / this@row
basics
~10 sThe 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 linestable { // this: Table
row { // this: Row
cell("a") // Row.cell
this@table // the enclosing Table receiver
}
}go deeper
Knows the block runs with this set to the object so row means Table.row.
Explains implicit-receiver resolution, innermost-wins, and receiver switching on nesting.
Adds labeled this@, the multiple-receivers-in-scope subtlety, and that it's all compile-time.
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