skip to content

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?

level: middleimportance: must knowfreq 55%

answer

  1. +"x" desugars to "x".unaryPlus()
  2. Member extension operator fun String.unaryPlus() on Tag
  3. this == String, this@Tag == enclosing tag
  4. Member scope means it only works inside a tag
  5. Must be marked operator; returns Unit

basics

~20 s

The 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 lines
kotlin
abstract 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 p

go deeper

for a junior

Recognizes +"text" as the unary-plus operator that adds text and knows it only works inside a tag.

for a middle

Implements operator fun String.unaryPlus() as a member extension, distinguishes the two receivers, and explains the readability/scoping rationale.

for a senior

Contrasts it with binary plus, explains member-extension scoping precisely, and notes how kotlinx.html standardizes the idiom.

for a principal

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

context