Explain the `unaryPlus` and `invoke` conventions and how they make builder-style DSLs read naturally (e.g. `+"text"` inside an HTML builder, or `dependencies { implementation(...) }`).
answer
- +x → unaryPlus(); builder text nodes (kotlinx.html)
- obj(args) → invoke(args); makes objects callable
- dependencies { } = invoke(block: Handler.() -> Unit)
- lambda-with-receiver sets implicit this for resolution
- @DslMarker hides outer receivers in nested blocks
basics
~20 sunaryPlus lets +x mean 'add x', so +"hello" inside a builder appends text. invoke lets you call an object like a function — thing(args) runs thing.invoke(args). Both remove method-name noise so DSL blocks read cleanly.
solid answer
~40 s`operator fun unaryPlus()` overloads prefix `+`: inside a builder receiver, `+"text"` becomes `text.unaryPlus()` — kotlinx.html uses this so `+ "Hello"` appends a text node to the current element. `operator fun invoke(...)` makes any object **callable**: `obj(x)` is `obj.invoke(x)`. Combined with a lambda-with-receiver parameter, `invoke` powers `dependencies { ... }`: `dependencies` is a property/object whose `invoke(block: DependencyHandler.() -> Unit)` runs the block against a handler. Together with `@DslMarker` to control implicit-receiver scope, these conventions let nested blocks read like configuration rather than method chains. The key is that the unary operator and `invoke` are resolved against the **implicit receiver** of the current lambda scope, so the punctuation-free calls bind to the right builder object.
code
kotlin · 18 lines@DslMarker annotation class HtmlDsl
@HtmlDsl
class Tag(val name: String) {
val children = mutableListOf<Any>()
operator fun String.unaryPlus() { children += this } // +"text"
operator fun invoke(block: Tag.() -> Unit) = apply(block) // tag { ... }
}
fun html(block: Tag.() -> Unit) = Tag("html").apply(block)
val page = html {
val body = Tag("body")
body { // invoke: body.invoke { ... }
+"Hello" // unaryPlus: appends text node
}
children += body
}go deeper
Recognizes +"text" and that objects can sometimes be called like functions, even if hazy on why.
Explains unaryPlus/invoke desugaring and connects them to builder blocks.
Ties them to lambda-with-receiver scoping and @DslMarker, and explains member-extension scoping for unaryPlus.
Weighs DSL ergonomics against discoverability/debuggability, and designs receiver/marker scoping to keep large DSLs safe and readable.
## `unaryPlus` The prefix `+` operator maps to `operator fun unaryPlus()`. Inside a **lambda with receiver**, the receiver is the implicit `this`, so a bare `+"text"` resolves to `this."text".unaryPlus()`... more precisely, the receiver type defines `unaryPlus` as an **extension on the element type**: ```kotlin class Tag { val children = mutableListOf<String>() operator fun String.unaryPlus() { children += this } // member extension } fun html(block: Tag.() -> Unit): Tag = Tag().apply(block) html { +"Hello" // [email protected] { "Hello".unaryPlus() } -> children += "Hello" } ``` Here `unaryPlus` is a **member extension function** (`String.unaryPlus` declared inside `Tag`), so it is only in scope when `Tag` is the receiver. That is exactly how **kotlinx.html** appends raw text nodes with `+"..."`. ## `invoke` `operator fun invoke(...)` makes an instance **callable** like a function: `x(a, b)` is `x.invoke(a, b)`. Any number of `invoke` overloads (with parameters) is allowed. ```kotlin object Greeter { operator fun invoke(name: String) = "Hi $name" } Greeter("Ada") // "Hi Ada" ``` This is how Gradle's Kotlin DSL turns a *property* into a *block*: `dependencies` is accessible as a value, and `operator fun invoke(block: DependencyHandler.() -> Unit)` lets you write `dependencies { ... }`. The `{ ... }` is the trailing-lambda argument to `invoke`, executed with a receiver so nested calls like `implementation("...")` resolve against the handler. ## Why these enable clean DSLs - **Lambda with receiver** (`T.() -> Unit`) sets an *implicit receiver*; punctuation-free calls (`+"x"`, `implementation(...)`) resolve against it. - `unaryPlus` removes the method name for the most common 'add a child' operation. - `invoke` removes the `.configure`/`.apply` noise, making `dependencies { }` look built-in. - **`@DslMarker`** is essential glue: annotate the builder types so that in a nested block, the *outer* receiver's members are hidden, preventing accidental calls like adding text to the wrong (grandparent) element. Without it, multiple implicit receivers stack and resolution becomes error-prone. ## Pitfalls - Overusing `invoke` makes call sites mysterious; readers can't tell a value is callable. - `unaryPlus` as a *member extension* limits its scope (good), but a top-level `unaryPlus` extension leaks everywhere (bad). - Multiple nested receivers without `@DslMarker` cause confusing resolution and the classic 'I added a child to the wrong node' bug.
- Why declare `unaryPlus` as a member extension (`operator fun String.unaryPlus()` inside the builder) instead of a top-level extension?Scoping. A member extension is only visible when the builder is the receiver, so `+"x"` only works inside the DSL block and doesn't pollute the global namespace.
- What problem does `@DslMarker` solve in invoke/receiver-based DSLs?With nested lambdas-with-receiver, multiple implicit receivers are in scope. `@DslMarker` restricts resolution to the nearest receiver, preventing accidental calls on an outer builder.
invoke is giving an object a doorbell so you can 'ring' it like a function; unaryPlus is a shorthand 'put this in' gesture inside a builder room.
saying these in an interview costs you the question
- Confusing `unaryPlus` (prefix +x) with binary `plus` (a + b)
- Thinking `invoke` requires a special interface rather than just the `operator` modifier
- Not knowing lambda-with-receiver provides the implicit `this` that these operators resolve against
- Ignoring `@DslMarker` and the multiple-implicit-receiver problem
- Claiming `dependencies { }` is special compiler magic rather than `invoke` + receiver lambda