skip to content

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(...) }`).

level: seniorimportance: should knowfreq 45%

answer

  1. +x → unaryPlus(); builder text nodes (kotlinx.html)
  2. obj(args) → invoke(args); makes objects callable
  3. dependencies { } = invoke(block: Handler.() -> Unit)
  4. lambda-with-receiver sets implicit this for resolution
  5. @DslMarker hides outer receivers in nested blocks

basics

~20 s

unaryPlus 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
kotlin
@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

for a junior

Recognizes +"text" and that objects can sometimes be called like functions, even if hazy on why.

for a middle

Explains unaryPlus/invoke desugaring and connects them to builder blocks.

for a senior

Ties them to lambda-with-receiver scoping and @DslMarker, and explains member-extension scoping for unaryPlus.

for a principal

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

context