skip to content

Beyond the receiver lambda, which Kotlin features turn nested builder calls into something that reads like a declarative language? Illustrate with `infix` and `unaryPlus`.

level: middleimportance: should knowfreq 45%

answer

  1. infix = drop dot & parens, one arg
  2. unaryPlus makes +"text" meaningful
  3. operator names are fixed (plus, invoke, get/set)
  4. trailing lambda + defaults clean the call site
  5. extensions add vocabulary without owning the type

basics

~20 s

Infix 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 s

Receiver 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 lines
kotlin
class 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

for a junior

Recognizes +"text" and a to b as Kotlin syntax sugar but may not name the underlying functions.

for a middle

Can implement an unaryPlus operator and an infix function and wire them into a receiver-lambda builder.

for a senior

Knows the infix rules, the fixed operator name set, and when readability sugar helps vs. obscures.

for a principal

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

context