Walk through exactly how the receiver lambda makes td { text = "x" } resolve text against the child node, and show how to make an apply-based one-liner builder.
answer
- Td.() -> Unit binds this to the child
- init(child) == child.init() == child.apply(init) (minus return)
- apply runs receiver block, returns object; also runs it-block, returns object
- Both inline → no allocation per node
- this@Tr reaches the outer receiver
basics
~20 sThe block you pass is a lambda whose 'this' is the child node. So inside td { }, text means the child's text. You can write the builder in one line with apply, which runs the block on the child and returns it.
solid answer
~40 sThe parameter type is Td.() -> Unit, a function type with receiver. When you call child.init() (or init(child)), the compiler binds this inside the lambda to child, so an unqualified text resolves to child.text — equivalent to this.text. apply is itself an extension defined as inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }, so Td().apply(init) runs init with the new Td as receiver and returns it. Combine with also (which returns the receiver after a side effect) to attach to the parent: fun td(init: Td.() -> Unit) = Td().apply(init).also { cells.add(it) }. The inline modifier on apply/also means no lambda object is allocated. Receiver lambdas are why nested builders read like a literal tree.
code
kotlin · 11 linesclass Tr {
val cells = mutableListOf<Td>()
// expression body: create, configure (apply), attach+return (also)
fun td(init: Td.() -> Unit): Td = Td().apply(init).also { cells.add(it) }
}
class Td { var text: String = ""; var colspan: Int = 1 }
val r = Tr().apply {
td { text = "A"; colspan = 2 } // text/colspan resolve to this Td
td { text = "B" }
}go deeper
Recognizes that text inside the block means the child's text but may not articulate the receiver mechanism.
Explains function-types-with-receiver, writes the apply/also one-liner, and knows inline avoids allocation.
Discusses receiver shadowing, this@Label qualification, and when to prefer init(child) for clarity.
Reasons about allocation/inlining impact at scale and API surface choices (return child vs Unit).
## Function types with receiver A type written `Td.() -> Unit` is a **function type with receiver**: a function that runs *as if it were a member of `Td`*. The receiver becomes `this` inside the body. So: ```kotlin val init: Td.() -> Unit = { text = "x" } // 'text' here is this.text on a Td ``` There are two equivalent ways to invoke it: - **member-style**: `child.init()` - **call-style**: `init(child)` — the receiver is passed as the first argument. Both bind `this` inside the lambda to `child`. That is the whole trick: `td { text = "x" }` desugars to passing the lambda `{ text = "x" }` as `init`, and the builder runs it with the freshly-created `Td` as receiver, so `text` (i.e. `this.text`) writes to that `Td`. ## apply and also Kotlin's standard scope functions make builders terse: ```kotlin public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this } public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this } ``` - `apply` runs a **receiver** lambda and returns the object (good for *configuring*). - `also` runs a lambda taking `it` and returns the object (good for a *side effect* like adding to a list). Both are `inline`, so the lambdas are inlined — **no extra object allocation** per builder call, which matters when building large trees. ## The one-liner ```kotlin class Tr { val cells = mutableListOf<Td>() fun td(init: Td.() -> Unit): Td = Td().apply(init).also { cells.add(it) } } ``` Read it as: create a `Td`, configure it with `init` (apply), add it to `cells` and return it (also). ## Why not just init(child)? You can: `fun td(init: Td.() -> Unit): Td { val c = Td(); c.init(); cells.add(c); return c }` is identical in behavior. The `apply`/`also` form is just idiomatic and expression-bodied. ## Gotcha: shadowing Inside a nested block you have *two* receivers in scope (parent and child). Unqualified names resolve to the **innermost** receiver. To reach the parent explicitly you use a qualified `this@Tr`. `@DslMarker` removes the implicit access to outer receivers to prevent mistakes.
- Why are apply and also marked inline, and does it matter for builders?inline copies the lambda body into the call site, avoiding a function-object allocation per call; for deeply nested trees built in hot paths this removes a lot of short-lived garbage.
- How do you reference the parent receiver from inside a child block?Use a qualified this — e.g. this@Tr — since the unqualified this is the innermost (child) receiver.
saying these in an interview costs you the question
- Saying the lambda parameter is named 'it' for the child (receiver lambdas use this, not it)
- Claiming apply returns Unit
- Thinking init(child) and child.init() behave differently
- Not knowing two receivers can be in scope inside a nested block
- Believing scope functions allocate a lambda object even though they're inline