SyntaxStudy
Sign Up
Kotlin Building DSLs with Extension Lambdas
Kotlin Beginner 1 min read

Building DSLs with Extension Lambdas

Function types with receivers, written as Type.() -> Unit, are the backbone of Kotlin DSLs. When you invoke such a lambda, the receiver object becomes this inside the block, bringing all its members into scope without qualification. This allows you to write builder APIs that read almost like a configuration language. Kotlin's standard library contains several DSL-like constructs built on this mechanism: buildString, buildList, buildMap, and the Gradle Kotlin DSL. The @DslMarker annotation restricts nested DSL usage to prevent accidentally calling outer-scope methods inside an inner block. Combining extension lambdas with reified type parameters and inline functions produces powerful type-safe builders used in frameworks such as Ktor (routing DSL) and Jetpack Compose (though Compose uses a different but related mechanism).
Example
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class Tag(val name: String) {
    private val children = mutableListOf<String>()
    private val attrs    = mutableMapOf<String, String>()

    fun attr(key: String, value: String) { attrs[key] = value }
    fun text(content: String)            { children += content }
    fun child(tag: Tag)                  { children += tag.render() }

    fun render(): String {
        val attrStr = attrs.entries.joinToString(" ") { (k, v) -> "$k=\"$v\"" }
        val open  = if (attrStr.isEmpty()) "<$name>" else "<$name $attrStr>"
        return "$open${children.joinToString("")}</$name>"
    }
}

fun tag(name: String, block: Tag.() -> Unit): Tag {
    val t = Tag(name)
    t.block()
    return t
}

// buildString DSL from stdlib
fun buildReport(items: List<Pair<String, Int>>): String = buildString {
    appendLine("=== Report ===")
    items.forEach { (label, value) ->
        append("  ").append(label).append(": ").appendLine(value)
    }
    append("Total: ").append(items.sumOf { it.second })
}

fun main() {
    val div = tag("div") {
        attr("class", "container")
        child(tag("h1") { text("Hello, DSL!") })
        child(tag("p")  { text("Kotlin makes this clean.") })
    }
    println(div.render())

    println()
    val report = buildReport(listOf("Sales" to 500, "Refunds" to 30, "Net" to 470))
    println(report)
}