In a nested receiver-lambda DSL, how does Kotlin resolve a call when multiple implicit receivers are in scope, and how do @DslMarker and labeled this (this@Outer) control that resolution?
answer
- Innermost receiver wins, then outward
- @DslMarker hides same-marker outer receivers
- this@Label targets a specific receiver
- Label = function/lambda name
- Escape hatch: explicit qualified this
basics
~10 sWhen nested receiver lambdas stack, Kotlin tries the innermost receiver first, then outer ones. @DslMarker blocks reaching outer receivers implicitly, and this@Label lets you target a specific one explicitly.
solid answer
~50 sInside nested receiver lambdas there are multiple implicit receivers. For an unqualified call, the compiler resolves against the innermost matching receiver, walking outward only if the inner one has no matching member. This is convenient but error-prone in DSLs: an inner block can silently call an outer builder's method. @DslMarker fixes this: you create a meta-annotation annotated with @DslMarker, apply it to builder types, and the compiler then makes only the nearest receiver of each marked type available implicitly; outer same-marker receivers require explicit qualification. To reach a specific receiver you use a labeled this: this@Html, this@Body, where the label is the function name that introduced that receiver (or an explicit lambda label). Implicit receivers also include extension and dispatch receivers; this@ClassName disambiguates among them. Understanding this resolution order is essential for writing safe, readable DSLs.
code
kotlin · 15 lines@DslMarker annotation class FormDsl
@FormDsl class Form { var action = ""; fun field(init: Field.() -> Unit) { Field().apply(init) } }
@FormDsl class Field { var name = "" }
fun form(init: Form.() -> Unit) = Form().apply(init)
form {
action = "/save"
field {
name = "email"
// action = "x" // ERROR with @DslMarker: outer Form receiver hidden
this@form.action = "/x" // OK: explicitly qualified to reach the Form
}
}go deeper
Aware that this refers to the current block's receiver in a DSL.
Knows @DslMarker improves nested-DSL safety and that this@Label exists.
Explains innermost-first resolution, exactly what @DslMarker restricts, and uses this@Label correctly as the escape hatch.
Designs marker hierarchies for multi-level DSLs and reasons about resolution corner cases and error ergonomics.
## Multiple implicit receivers Each receiver lambda introduces an **implicit receiver** — a `this` you can omit. When lambdas nest, several receivers are simultaneously in scope: ```kotlin html { // this: Html body { // this: Body, but Html is still in scope // calls resolve against Body first, then Html } } ``` ## Resolution order For an **unqualified** member call, the compiler searches implicit receivers from **innermost to outermost** and picks the first whose type declares a matching member. If `Body` has `p()`, `p("x")` hits `Body`. But if only the outer `Html` had a method named `meta()`, calling `meta()` inside `body { }` would silently bind to the **outer** `Html` receiver — usually a bug. ## @DslMarker `@DslMarker` is a meta-annotation that scopes implicit receivers: ```kotlin @DslMarker annotation class HtmlDsl @HtmlDsl class Html { fun body(init: Body.() -> Unit) {} } @HtmlDsl class Body { fun p(t: String) {} } ``` Rule: within a block, **only the nearest implicit receiver of each `@DslMarker`-marked annotation is accessible implicitly**. Outer receivers carrying the **same** marker are hidden — calling them requires an explicit qualified `this`. This turns the silent-outer-call bug into a compile error, while still allowing intentional access via qualification. ## Labeled this — this@Label To target a specific receiver explicitly, use a **qualified this**: ```kotlin html { body { [email protected]() // reach the outer Html receiver [email protected]("hi") // the inner Body receiver } } ``` The label after `@` is the **name of the function/lambda that introduced the receiver** (for extension/DSL functions it's the function name; you can also add an explicit lambda label like `body@`). `this@ClassName` is also used inside a class to refer to the outer class instance or to disambiguate dispatch vs extension receivers. ## Why it matters - Without `@DslMarker`, large DSLs leak outer scope and produce confusing bugs. - With it, the DSL is **scope-safe** by default, and `this@Outer` is the explicit escape hatch. ## Key terms - **Implicit receiver**: a `this` accessible without naming it. - **@DslMarker**: restricts implicit receivers of marked types to the nearest one. - **Qualified/labeled this**: `this@Label` to pick a specific receiver.
- Without @DslMarker, what would calling an outer-only method from an inner block do?It compiles and silently binds to the outer implicit receiver, a common source of subtle DSL bugs that @DslMarker turns into a compile error.
- What is the label in this@html referring to?The name of the DSL function that introduced that receiver (here html). It selects that specific implicit receiver among the stacked ones.
saying these in an interview costs you the question
- Claiming outer receivers shadow inner ones (it's the reverse)
- Thinking @DslMarker is required for any DSL to compile
- Not knowing this@Label exists as the escape hatch
- Saying @DslMarker removes outer receivers entirely (only implicit access is blocked)