← thecodex.expert · The Codex Family of Knowledge
Kotlin

Kotlin DSL

build.gradle.kts’s entire readable, nested syntax isn’t a special language feature just for Gradle — it’s ordinary lambdas with a receiver, a pattern available to any Kotlin code you write yourself.

Kotlin 2.1 Lambdas with a receiver Last verified:
Canonical Definition

A function type like HtmlBuilder.() -> Unit describes a lambda with a receiver — inside its body, this refers to an HtmlBuilder instance, so the lambda can call HtmlBuilder's own members directly, with no prefix at all, exactly as if the code were written inside HtmlBuilder itself. Combined with trailing lambda syntax, this is what produces the nested, keyword-like syntax of a DSL: div { p { text("hello") } } is ordinary function calls underneath, not special language syntax. @DslMarker, applied to an annotation used on the builder classes, prevents an inner lambda from accidentally calling an OUTER receiver's members — a real, if subtle, scope-leakage problem that arises naturally when DSL builders nest.

Lambda with a receiver: the core mechanism

The lambda type is Int.() -> String, not (Int) -> String — inside the lambda, this refers to the Int itself, so its own methods (like toString()) are callable directly with no prefix.

Kotlinreceiver_lambda.kt
fun describe(n: Int, block: Int.() -> String): String {
    return n.block()   // called ON n — n becomes 'this' inside block
}

val result = describe(5) {
    "The number is ${this}, squared is ${this * this}"   // 'this' is the Int itself
}
println(result)   // "The number is 5, squared is 25"

Building a simple HTML-style DSL

div { p("Hello") } reads like a mini-language, but it's entirely ordinary Kotlin functions and lambdas with receivers underneath — no special DSL syntax exists in the language itself.

Kotlinsimple_html_dsl.kt
class Tag(val name: String) {
    val children = mutableListOf<String>()
    fun p(text: String) { children.add("<p>$text</p>") }
    override fun toString() = "<$name>${children.joinToString("")}</$name>"
}

fun div(block: Tag.() -> Unit): Tag {
    val tag = Tag("div")
    tag.block()   // 'this' inside block is the Tag — p() is callable directly
    return tag
}

val html = div {
    p("Hello")
    p("World")
}
println(html)   // <div><p>Hello</p><p>World</p></div>

@DslMarker: preventing accidental outer-scope access

Without @DslMarker, a lambda nested inside another builder could accidentally call the OUTER builder's methods too — a genuinely confusing possibility once DSL builders nest more than one level deep. The annotation restricts each nested scope to only its own immediate receiver.

Kotlindsl_marker.kt
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class Tag(val name: String) { /* ... */ }
// with @HtmlDsl applied, a nested tag's lambda can no longer implicitly reach
// an OUTER tag's members — each level of nesting is scoped to its own receiver only

Sources

1
JetBrains. Kotlin Documentation, "Type-safe builders," kotlinlang.org/docs/type-safe-builders.html.