Reading — step 1 of 5
Learn
~1 min readInline + DSL Building
Kotlin's combination of lambdas with receivers, infix functions, operator overloading, and trailing lambdas makes type-safe DSLs natural.
class HtmlBuilder {
private val children = mutableListOf<String>()
fun head(block: HeadBuilder.() -> Unit) {
children.add(HeadBuilder().apply(block).build())
}
fun body(block: BodyBuilder.() -> Unit) {
children.add(BodyBuilder().apply(block).build())
}
fun build(): String = "<html>${children.joinToString("")}</html>"
}
class HeadBuilder {
var title: String = ""
fun build(): String = "<head><title>$title</title></head>"
}
class BodyBuilder {
private val parts = mutableListOf<String>()
fun p(text: String) { parts += "<p>$text</p>" }
fun build(): String = "<body>${parts.joinToString("")}</body>"
}
fun html(block: HtmlBuilder.() -> Unit): String =
HtmlBuilder().apply(block).build()
// Use:
val doc = html {
head { title = "Hello" }
body {
p("First paragraph")
p("Second")
}
}
The two key tricks:
- Lambda with receiver
Builder.() -> Unit— the lambda runs as if it's a method on Builder, sotitle = "..."works. - Trailing lambda —
html { ... }is sugar forhtml({ ... }).
@DslMarker prevents accidental scope leaking — a common DSL bug:
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HtmlBuilder { ... }
@HtmlDsl
class HeadBuilder { ... }
// Now this is a compile error:
html {
head {
title = "x"
p("oops") // ERROR: p is on BodyBuilder, can't be reached from HeadBuilder scope
}
}
Gradle's Kotlin DSL, Jetpack Compose, kotlinx.html — all use this pattern.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…