In a kotlinx.html-style DSL like html { body { p { +"hi" } } }, what Kotlin language feature makes the nested blocks work, and why can each block call functions like body or p without a qualifier?
answer
- Lambda with receiver: T.() -> Unit
- Inside block this == the builder node
- Each tag fn: create child, attach, run block on it
- Unqualified calls resolve against nearest implicit receiver
- apply{} is the same shape
basics
~10 sEach block is a lambda that runs 'on' an object (a receiver). Inside the block you can call that object's functions directly, so body() and p() are methods of the current builder.
solid answer
~40 sThe blocks are function literals with receiver: a lambda typed as T.() -> Unit. The function html(block: HTML.() -> Unit) calls block on a fresh HTML instance, so inside the block this is that HTML and you can call its member/extension functions (body, etc.) without a qualifier. Each builder method (body, p) takes its own receiver lambda and creates the matching child node, attaching it to the parent before invoking the block. Nesting just chains these typed receivers: outer this is HTML, inner this is BODY, then P. Kotlin resolves an unqualified call against the nearest receiver in scope, which is what gives the indented, tree-shaped syntax.
code
kotlin · 13 linesfun html(block: HTML.() -> Unit): HTML = HTML().apply(block)
class HTML {
val children = mutableListOf<Any>()
fun body(block: BODY.() -> Unit): BODY = BODY().also { children += it }.apply(block)
}
class BODY {
val children = mutableListOf<Any>()
fun p(block: P.() -> Unit): P = P().also { children += it }.apply(block)
}
class P { val children = mutableListOf<Any>() }
val doc = html { body { p { } } }go deeper
Knows the blocks are lambdas with a receiver and that this is the current node, enabling unqualified calls.
Can write the html()/body() functions, explain apply, and describe how each tag attaches a child then runs the block on it.
Explains implicit-receiver resolution order, why outer receivers leak into inner blocks, and connects it to stdlib scope functions.
Discusses inline-ability, allocation/escape implications of receiver lambdas, and how this mechanism generalizes to any tree-building or config DSL.
## The core idea: function types with receiver A normal lambda has type `(Args) -> R`. A **function literal with receiver** has type `T.(Args) -> R` — it behaves like an extension function on `T`. Inside such a lambda, `this` refers to the receiver `T`, so you can call `T`'s members and extensions **without a qualifier**. ```kotlin fun html(block: HTML.() -> Unit): HTML { val node = HTML() node.block() // invoke the lambda with node as the receiver (this == node) return node } ``` Inside `html { ... }` the receiver `this` is the `HTML` node, so unqualified calls resolve against it. ## Each builder method takes its own receiver lambda Every tag function follows the same shape: create the child, attach it to the parent, then run the user's block on the child. ```kotlin class HTML : Tag("html") { fun body(block: BODY.() -> Unit): BODY { val b = BODY() children += b // attach to the tree b.block() // now this == b inside the user block return b } } ``` So `html { body { p { ... } } }` becomes: `this` is `HTML`, then inside `body { }` it shifts to `BODY`, then inside `p { }` to `P`. The indentation mirrors the node tree. ## Why unqualified calls work — receiver scoping Kotlin resolves an unqualified call `body()` by searching the **implicit receivers** in scope, nearest first. Because each block installs a new receiver, the available builder functions change as you descend. This is also why you can accidentally call an *outer* receiver's function from an inner block — the outer `this` is still in scope (unless restricted by `@DslMarker`). ## Key APIs / keywords - `HTML.() -> Unit` — function type **with receiver**. - `this` / `this@HTML` — implicit and labeled receivers. - `apply { }` in the stdlib is the same pattern: `inline fun <T> T.apply(block: T.() -> Unit): T`. This receiver-lambda mechanism, not macros or codegen, is the entire foundation of type-safe builder DSLs.
- Why is apply often used to implement these builder functions?apply has signature T.(T.() -> Unit): T — it runs the block with the object as receiver and returns the object, exactly the create-configure-return pattern tags need.
- What does the type HTML.() -> Unit mean compared to (HTML) -> Unit?Both take one HTML; the receiver form makes it the implicit this so you call its members unqualified, while (HTML) -> Unit passes it as a named parameter you must reference explicitly.
Each block is like walking into a room: while you're inside, you can flip that room's switches by name without saying which room.
saying these in an interview costs you the question
- Saying it relies on reflection, macros, or annotation processing
- Confusing T.() -> Unit with (T) -> Unit
- Claiming body/p are global functions rather than receiver members
- Not knowing this changes as you nest
- Thinking the indentation is enforced by the compiler