66. Building a Type-Safe DSL

A lambda with receiver, typed T.() -> Unit, lets you call a builder’s methods directly inside a { } block, as if you were writing in that object’s scope. This is the foundation of Kotlin’s type-safe DSLs, like the HTML builder below.

class Paragraph(val text: String) {
    override fun toString() = "<p>$text</p>"
}

class Body {
    private val paragraphs = mutableListOf<Paragraph>()
    fun p(text: String) {            // available inside body { }
        paragraphs += Paragraph(text)
    }
    override fun toString() = "<body>${paragraphs.joinToString("")}</body>"
}

class Html {
    private var body: Body? = null
    fun body(init: Body.() -> Unit) {   // receiver lambda
        body = Body().apply(init)       // run the block on a fresh Body
    }
    override fun toString() = "<html>${body ?: ""}</html>"
}

fun html(init: Html.() -> Unit): Html = Html().apply(init)

Each builder function takes a receiver lambda and runs it with apply, which returns the configured object. The result reads like markup, yet the compiler checks every call — p only resolves inside a body { } block.

fun main() {
    val page = html {
        body {
            p("Hello")
            p("World")
        }
    }
    println(page)
}

Running it:

$ kotlin run
<html><body><p>Hello</p><p>World</p></body></html>
← Prev Index Next →