skip to content

HTML / Markup DSL Pattern

The markup DSL pattern nests receiver lambdas per tag, uses @DslMarker to keep scopes honest, and overloads unaryPlus so a bare string becomes a text child. It is the canonical example because it exercises every DSL feature at once.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

In a kotlinx.html-style DSL like html { body { p { +"hi" } } }, what Kotlin language feature makes the nested blocks work, and why can each block call functions like body or p without a qualifier?

level: juniorimportance: must knowfreq 60%

answer

  1. Lambda with receiver: T.() -> Unit
  2. Inside block this == the builder node
  3. Each tag fn: create child, attach, run block on it
  4. Unqualified calls resolve against nearest implicit receiver
  5. apply{} is the same shape

basics

~10 s

Each block is a lambda that runs 'on' an object (a receiver). Inside the block you can call that object's functions directly, so body() and p() are methods of the current builder.

solid answer

~40 s

The blocks are function literals with receiver: a lambda typed as T.() -> Unit. The function html(block: HTML.() -> Unit) calls block on a fresh HTML instance, so inside the block this is that HTML and you can call its member/extension functions (body, etc.) without a qualifier. Each builder method (body, p) takes its own receiver lambda and creates the matching child node, attaching it to the parent before invoking the block. Nesting just chains these typed receivers: outer this is HTML, inner this is BODY, then P. Kotlin resolves an unqualified call against the nearest receiver in scope, which is what gives the indented, tree-shaped syntax.

code

kotlin · 13 lines
kotlin
fun html(block: HTML.() -> Unit): HTML = HTML().apply(block)

class HTML {
    val children = mutableListOf<Any>()
    fun body(block: BODY.() -> Unit): BODY = BODY().also { children += it }.apply(block)
}
class BODY {
    val children = mutableListOf<Any>()
    fun p(block: P.() -> Unit): P = P().also { children += it }.apply(block)
}
class P { val children = mutableListOf<Any>() }

val doc = html { body { p { } } }

go deeper

for a junior

Knows the blocks are lambdas with a receiver and that this is the current node, enabling unqualified calls.

for a middle

Can write the html()/body() functions, explain apply, and describe how each tag attaches a child then runs the block on it.

for a senior

Explains implicit-receiver resolution order, why outer receivers leak into inner blocks, and connects it to stdlib scope functions.

for a principal

Discusses inline-ability, allocation/escape implications of receiver lambdas, and how this mechanism generalizes to any tree-building or config DSL.

## The core idea: function types with receiver A normal lambda has type `(Args) -> R`. A **function literal with receiver** has type `T.(Args) -> R` — it behaves like an extension function on `T`. Inside such a lambda, `this` refers to the receiver `T`, so you can call `T`'s members and extensions **without a qualifier**. ```kotlin fun html(block: HTML.() -> Unit): HTML { val node = HTML() node.block() // invoke the lambda with node as the receiver (this == node) return node } ``` Inside `html { ... }` the receiver `this` is the `HTML` node, so unqualified calls resolve against it. ## Each builder method takes its own receiver lambda Every tag function follows the same shape: create the child, attach it to the parent, then run the user's block on the child. ```kotlin class HTML : Tag("html") { fun body(block: BODY.() -> Unit): BODY { val b = BODY() children += b // attach to the tree b.block() // now this == b inside the user block return b } } ``` So `html { body { p { ... } } }` becomes: `this` is `HTML`, then inside `body { }` it shifts to `BODY`, then inside `p { }` to `P`. The indentation mirrors the node tree. ## Why unqualified calls work — receiver scoping Kotlin resolves an unqualified call `body()` by searching the **implicit receivers** in scope, nearest first. Because each block installs a new receiver, the available builder functions change as you descend. This is also why you can accidentally call an *outer* receiver's function from an inner block — the outer `this` is still in scope (unless restricted by `@DslMarker`). ## Key APIs / keywords - `HTML.() -> Unit` — function type **with receiver**. - `this` / `this@HTML` — implicit and labeled receivers. - `apply { }` in the stdlib is the same pattern: `inline fun <T> T.apply(block: T.() -> Unit): T`. This receiver-lambda mechanism, not macros or codegen, is the entire foundation of type-safe builder DSLs.

  • Why is apply often used to implement these builder functions?
    apply has signature T.(T.() -> Unit): T — it runs the block with the object as receiver and returns the object, exactly the create-configure-return pattern tags need.
  • What does the type HTML.() -> Unit mean compared to (HTML) -> Unit?
    Both take one HTML; the receiver form makes it the implicit this so you call its members unqualified, while (HTML) -> Unit passes it as a named parameter you must reference explicitly.

Each block is like walking into a room: while you're inside, you can flip that room's switches by name without saying which room.

saying these in an interview costs you the question

  • Saying it relies on reflection, macros, or annotation processing
  • Confusing T.() -> Unit with (T) -> Unit
  • Claiming body/p are global functions rather than receiver members
  • Not knowing this changes as you nest
  • Thinking the indentation is enforced by the compiler

context

open as a page

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%

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.

open as a page

Without @DslMarker, what bug can appear in a nested markup DSL, and how does annotating tag types with a @DslMarker annotation fix it?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Without it, an inner block can accidentally call functions from an outer tag, building a wrong tree. @DslMarker tells the compiler to hide outer receivers inside nested blocks, so only the closest tag's functions are available implicitly.

open as a page

Sketch a minimal type-safe HTML builder so that html { body { p { +"Hello" } } } compiles and can render to a string. Identify the receiver lambdas, the child-attachment step, and the unaryPlus.

level: middleimportance: should knowfreq 40%

basics

~20 s

Make a Tag class holding children. Each tag function creates a child, adds it to children, runs the block on it, and returns it. Add operator fun String.unaryPlus() to append text. A render() walks children to build the HTML string.

open as a page

What are the practical trade-offs of a kotlinx.html-style markup DSL versus templating, and what design choices (inline lambdas, @DslMarker discipline, escaping, builder return types) most affect its quality?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A Kotlin DSL gives type safety, refactoring, and IDE help but couples markup to code and adds a learning curve. Good DSLs use @DslMarker for scope safety, escape output, keep blocks inline, and return useful nodes.

open as a page