In the markup DSL the text inside a tag is written as +"hello". What operator is this, how do you implement it, and why is it used instead of a plain function call?
answer
- +"x" desugars to "x".unaryPlus()
- Member extension operator fun String.unaryPlus() on Tag
- this == String, this@Tag == enclosing tag
- Member scope means it only works inside a tag
- Must be marked operator; returns Unit
basics
~20 sThe plus sign is the unary-plus operator. You define operator fun String.unaryPlus() on the tag class so it adds the string as a text child. It's used because it reads cleanly and only works inside a tag.
solid answer
~40 s+"hello" is the prefix unary-plus operator, desugared by the compiler to "hello".unaryPlus(). You implement it as a member operator fun String.unaryPlus() on the tag/builder type, so it's only in scope when the current receiver is a tag. Its body appends a TextNode (or raw string) to the tag's children list. It's preferred over something like text("hello") because it's terser, visually separates content from structure, and — being a member extension — is automatically scoped: outside a tag receiver the operator simply isn't resolvable, so you can't accidentally call it. kotlinx.html defines exactly this on its Tag base type. The operator must return Unit-ish (typically Unit) since you discard the result.
code
kotlin · 8 linesabstract class Tag(val name: String) {
val children = mutableListOf<Any>()
fun p(block: Tag.() -> Unit) { children += Tag("p").apply(block) }
operator fun String.unaryPlus() { children += this }
}
fun html(block: Tag.() -> Unit) = Tag("html").apply(block)
val doc = html { p { +"Hello, world" } } // adds "Hello, world" as a child of pgo deeper
Recognizes +"text" as the unary-plus operator that adds text and knows it only works inside a tag.
Implements operator fun String.unaryPlus() as a member extension, distinguishes the two receivers, and explains the readability/scoping rationale.
Contrasts it with binary plus, explains member-extension scoping precisely, and notes how kotlinx.html standardizes the idiom.
Weighs operator-driven ergonomics vs. explicit text() for tooling/readability and discusses consistency of operator conventions across a DSL surface.
## What +"text" actually is `+expr` is the **unary plus** operator. Kotlin compiles it to a call to `expr.unaryPlus()`. By convention all overloadable operators map to specially named functions marked with the `operator` keyword: - `+a` -> `a.unaryPlus()` - `-a` -> `a.unaryMinus()` - `!a` -> `a.not()` So `+"hello"` is literally `"hello".unaryPlus()`. ## Implementing it for a markup DSL You declare it as a **member extension** on the tag base class, so it has access to the tag's child list yet is only resolvable when a tag is the receiver: ```kotlin abstract class Tag(val name: String) { val children = mutableListOf<Any>() operator fun String.unaryPlus() { children += TextNode(this) // 'this' is the String; outer Tag is this@Tag } } class TextNode(val text: String) ``` Note the two receivers: inside `unaryPlus`, `this` is the `String`, and `this@Tag` (the dispatch receiver) is the enclosing tag whose `children` you mutate. ## Why an operator instead of a function - **Readability:** `+"Welcome"` separates literal content from structural calls like `p { }`. The leading `+` is a visual marker for text. - **Scoping for free:** because it's a *member* extension on `Tag`, the operator only exists where a `Tag` is in scope. Write `+"x"` at top level and it won't compile — there's no receiver providing `unaryPlus` for `String`. - **Convention:** kotlinx.html ships `operator fun String.unaryPlus()` on its `Tag`, so the idiom is recognizable. ## Gotchas - Don't confuse it with `String.plus` (binary `+` for concatenation) — that's `a.plus(b)`. - The operator must be marked `operator`, else `+"x"` won't compile. - It returns `Unit` here; any return value would be discarded. - Because it's an *extension on String*, you could in principle define a competing top-level `unaryPlus` — keep it a member to retain scoping. ## Related operators in DSLs The same trick powers other DSL niceties: `operator fun T.unaryMinus()`, `operator fun invoke(...)`, and `infix` functions. The markup DSL specifically standardizes on `unaryPlus` for appending text children.
- Why declare unaryPlus as a member extension rather than a top-level extension on String?A member extension is only resolvable when its dispatch receiver (the Tag) is in scope, so the operator is naturally confined to inside a tag and can reach the tag's children.
- How does the compiler turn +"hi" into a call?It rewrites the prefix-plus expression to "hi".unaryPlus(), then resolves unaryPlus among the receivers in scope.
The leading + is like a quotation mark in the markup: it flags 'this is literal content' versus structure.
saying these in an interview costs you the question
- Calling it the binary plus / String concatenation operator
- Forgetting the operator keyword
- Not knowing inside the operator this is the String and this@Tag is the tag
- Claiming it's special compiler magic unique to kotlinx.html
- Saying it must return the text node