Type-Safe Builders and DSLs
|
This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A lambda with receiver is the single mechanism behind every "DSL-looking" Kotlin API — build.gradle.kts,
kotlinx.html, Ktor’s routing block — and it is worth understanding in isolation from any one of those
libraries.
Lambdas with Receiver
An ordinary function type is (T) → R; a function type with receiver is written T.() → R — inside such a
lambda, members of T are callable directly (via an implicit this), exactly as if the lambda’s body were
written inside a member function of T:
class HtmlBuilder {
private val content = StringBuilder()
fun text(value: String) {
content.append(value)
}
override fun toString() = content.toString()
}
fun html(block: HtmlBuilder.() -> Unit): String { // "block" has HtmlBuilder as its receiver
val builder = HtmlBuilder()
builder.block() // calling the lambda AS IF it were a member of builder
return builder.toString()
}
val page = html {
text("Hello, ") // "text" is called with no explicit receiver -- resolved against the implicit "this"
text("World!")
}
This is exactly the same mechanism run/with/apply
(Extension Functions and Scope Functions)
use internally — a scope function’s lambda parameter is itself typed T.() → R.
Building a Small Type-Safe HTML Builder
Nesting builder classes, each exposing member functions that themselves take a lambda-with-receiver, produces a
tree-shaped DSL where the Kotlin compiler enforces the structure (only a <li> inside a <ul>, say) at
compile time — unlike a plain string-templating approach, which catches nothing until the output is rendered:
class Ul {
private val items = mutableListOf<String>()
fun li(text: String) { items += "<li>$text</li>" }
override fun toString() = "<ul>${items.joinToString("")}</ul>"
}
class Html {
private val parts = mutableListOf<String>()
fun ul(block: Ul.() -> Unit) { parts += Ul().apply(block).toString() }
override fun toString() = parts.joinToString("")
}
fun html(block: Html.() -> Unit): String = Html().apply(block).toString()
val page = html {
ul {
li("first")
li("second")
}
}
// "<ul><li>first</li><li>second</li></ul>"
Each nested block call chains to a new receiver (Ul, not Html) for its own body — li(…) is only
callable inside a ul { } block, which is the compile-time structural guarantee this style buys over generating
markup with raw string concatenation.
Where This Pattern Shows Up
-
kotlinx.html— an HTML-generation library built on exactly this pattern, one builder class per HTML element. -
The Gradle Kotlin DSL (
build.gradle.kts) —plugins { },dependencies { }, andtasks.named { }are all lambda-with-receiver blocks over Gradle’s own configuration objects, which is what givesbuild.gradle.ktsIDE autocompletion a Groovybuild.gradlenever had. -
Routing DSLs (e.g. Ktor’s
routing { get("/users") { … } }) — the same nested-receiver structure, scoped to defining HTTP routes instead of markup.
See Also
-
Extension Functions and Scope Functions —
apply, used above to configure and return a builder in one expression. -
Lambdas and Higher-Order Functions — ordinary lambdas and function types, which a lambda with receiver extends.
-
Build and Tooling — the Gradle Kotlin DSL in practice.
References
-
Kotlin docs — Function types (including receiver-qualified function types).