Beyond the receiver lambda, which Kotlin features turn nested builder calls into something that reads like a declarative language? Illustrate with `infix` and `unaryPlus`.
answer
- infix = drop dot & parens, one arg
- unaryPlus makes +"text" meaningful
- operator names are fixed (plus, invoke, get/set)
- trailing lambda + defaults clean the call site
- extensions add vocabulary without owning the type
basics
~20 sInfix functions let you drop the dot and parentheses, so a to b reads like words. Operator functions like unaryPlus let +"text" mean an action. Together with receiver lambdas they make blocks read like configuration sentences.
solid answer
~40 sReceiver lambdas give you the implicit `this`; the *readability* on top comes from: (1) **`infix` functions** — a member or extension marked `infix` with one parameter can be called as `receiver name arg`, so `"key" to value` or `column width 100` reads like prose; (2) **operator overloading**, especially `operator fun unaryPlus()`, which makes the prefix `+x` a meaningful call — kotlinx.html uses `+"text"` to append a text node; (3) **trailing-lambda + default arguments** to keep call sites uncluttered; (4) **extension functions/properties** so the DSL vocabulary lives outside the core types. All of these are ordinary Kotlin; the DSL is the *combination*. `@DslMarker` then keeps nested scopes honest. None of this is special parsing — it is method calls dressed up by syntax rules.
code
kotlin · 12 linesclass Body {
val nodes = mutableListOf<String>()
operator fun String.unaryPlus() { nodes.add(this) }
infix fun String.repeated(n: Int) { repeat(n) { nodes.add(this@repeated) } }
}
fun body(block: Body.() -> Unit) = Body().apply(block)
body {
+"line" // unaryPlus
"dash" repeated 3 // infix
}go deeper
Recognizes +"text" and a to b as Kotlin syntax sugar but may not name the underlying functions.
Can implement an unaryPlus operator and an infix function and wire them into a receiver-lambda builder.
Knows the infix rules, the fixed operator name set, and when readability sugar helps vs. obscures.
Judges DSL ergonomics holistically — discoverability, IDE support, onboarding cost of clever operators.
## The supporting cast The receiver lambda is the foundation, but several other features supply the 'reads like English' feel. ### infix functions A function declared `infix` (a member or extension with exactly one non-default parameter, no vararg) can be called without the dot and parentheses: ```kotlin infix fun String.to(v: Int) = Pair(this, v) val p = "width" to 100 // instead of "width".to(100) ``` The standard `to` that builds map entries (`mapOf("a" to 1)`) is exactly this. In a DSL you write domain verbs as infix: `cell span 2`, `task dependsOn other`. ### operator overloading — `unaryPlus` Kotlin maps fixed names to operators. `operator fun unaryPlus()` makes the prefix `+` meaningful: ```kotlin class P { val children = mutableListOf<String>() operator fun String.unaryPlus() { children.add(this) } } // inside a P receiver: +"hello" // calls "hello".unaryPlus() -> adds text node ``` kotlinx.html uses exactly this so text content reads as `+"some text"`. Other handy operators: `plusAssign` (`+=`), `invoke` (`obj(args)`), `get`/`set` (`obj[key]`). ### trailing lambdas and defaults Because the last lambda argument can move outside the parentheses, and other args can have defaults, calls collapse to `p { +"text" }` instead of `p(klass = null, builder = { ... })`. ### extension functions/properties DSL vocabulary is often added via extensions so you don't have to own the receiver type. Gradle's Kotlin DSL adds extensions onto `Project`/`DependencyHandler`. ## Putting it together ```kotlin table { row { cell { +"Name" } span 2 // infix span on the cell result cell { +"Age" } } } ``` Each `{ }` is a receiver lambda, `+"..."` is `unaryPlus`, `span 2` is `infix`. The result reads declaratively, yet every token is a normal call. ## Guardrail: @DslMarker Once receivers nest, mark builder types with a `@DslMarker`-annotated annotation so an inner block cannot implicitly call an outer receiver's members — this prevents nonsense like calling `row { }` from inside a `cell { }`.
- What are the rules for a function to be usable as `infix`?It must be marked `infix`, be a member function or an extension function, take exactly one parameter, and that parameter must not be vararg or have a default value.
- Why use `unaryPlus` instead of a normal method like `text("...")`?Purely readability/density — `+"x"` is shorter and visually marks 'content' in markup-style DSLs. Both are valid; `text("x")` is clearer for newcomers.
saying these in an interview costs you the question
- Claiming you can invent arbitrary operator symbols
- Saying infix works with two or zero parameters
- Thinking unaryPlus requires the standard library's Int/String to change
- Forgetting these are still ordinary function calls