Show how fun html(block: HTML.() -> Unit) lets callers write html { body { ... } } with unqualified member calls. Why does the unqualified call work?
answer
- Create instance, run block on it, return it
- block: HTML.()->Unit makes HTML the this inside { }
- head { } means this.head { }
- Each builder fn supplies a new receiver for its block
- kotlinx.html is the canonical example
basics
~20 sThe function gives the lambda an HTML object as its hidden 'this'. So inside the braces you can call HTML's methods like body() directly, with no object name in front. That is how the builder reads like English.
solid answer
~40 s`fun html(block: HTML.() -> Unit): HTML` creates an HTML instance, invokes `block` on it (`html.block()` or `block(html)`), and returns it. Because `block`'s type is `HTML.() -> Unit`, inside the `{ }` the HTML instance is the implicit receiver `this`, so `body { ... }` is really `this.body { ... }` — an unqualified call to a member of HTML. The nested `body` member itself takes a `BODY.() -> Unit`, repeating the pattern one level down, which is how builders nest. This is the standard type-safe builder shape (kotlinx.html). The function typically returns the receiver so callers can chain or read the result.
code
kotlin · 9 linesclass TABLE { fun tr(block: TR.() -> Unit) { TR().apply(block) } }
class TR { fun td(text: String) { println(" td: $text") } }
fun table(block: TABLE.() -> Unit): TABLE = TABLE().apply(block)
table {
tr { td("a"); td("b") } // this is TABLE, then inside tr { } this is TR
tr { td("c") }
}go deeper
Recognizes the html { body { } } shape reads naturally but may not explain why.
Explains that the block's receiver type makes members unqualified and that nesting swaps receivers.
Implements the builder cleanly with apply, returns the receiver, and notes where @DslMarker would be added.
Designs the full DSL surface (which members are receiver lambdas vs. simple calls) and weighs readability, scoping, and misuse risks.
## The builder shape A **type-safe builder** is a function that takes a **receiver lambda** describing the contents of an object, runs that lambda against a fresh instance, and returns the populated instance. ```kotlin class HTML { fun head(block: HEAD.() -> Unit) { /* create HEAD, run block on it, attach */ } fun body(block: BODY.() -> Unit) { /* create BODY, run block on it, attach */ } } class HEAD { fun title(text: String) { /* ... */ } } class BODY { fun p(text: String) { /* ... */ } } fun html(block: HTML.() -> Unit): HTML { val root = HTML() root.block() // run the user's lambda with root as the receiver return root } ``` Usage: ```kotlin html { head { title("Hi") } // this.head { ... }; inside, this is HEAD, so title(...) is HEAD.title body { p("Hello") } // this.body { ... }; inside, this is BODY, so p(...) is BODY.p } ``` ## Why the unqualified call works `block: HTML.() -> Unit` means: when `root.block()` runs, the lambda body executes **with `root` as the implicit receiver `this`**. An unqualified name like `head` is resolved against the implicit receiver, so `head { ... }` compiles to `this.head { ... }`. No `it`, no variable name — the member is in scope as if you were writing code inside `HTML`. Nesting works because `head`'s parameter is itself a receiver lambda `HEAD.() -> Unit`. When `head` runs its block, **the new implicit receiver inside those braces is a HEAD**, so `title(...)` resolves to `HEAD.title`. Each builder function swaps in a new receiver for its own block. ## Returning the receiver Builders usually `return root` so the result is usable (render it, chain it). The internal call `root.block()` can also be written `block(root)` — equivalent for a receiver type. ## APIs/keywords in play - Function type with receiver: `HTML.() -> Unit`. - Implicit receiver `this` and unqualified member resolution. - Often combined with `@DslMarker` to prevent accidentally calling an outer receiver's members (a separate concern — scope control). ## Common real example kotlinx.html uses exactly this pattern: `html { head { } body { } }`. Anko (legacy) and many Gradle Kotlin DSL blocks follow the same idea.
- Why do builder functions usually return the receiver?So the caller can use, render, or chain the fully-constructed object; apply(block) conveniently returns the receiver it was called on.
- How does nesting end up swapping the implicit receiver?Each member's parameter is its own receiver lambda (e.g. body: BODY.() -> Unit), so inside that block the new instance (BODY) is the implicit receiver.
saying these in an interview costs you the question
- Says you must qualify calls like html.body { }
- Cannot explain that head { } is this.head { }
- Thinks one receiver is shared across all nesting levels
- Believes the builder must be a top-level function (it can be a member or extension)