Skip to content
Type-Safe Builders (DSLs)
step 1/5

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:

  1. Lambda with receiver Builder.() -> Unit — the lambda runs as if it's a method on Builder, so title = "..." works.
  2. Trailing lambdahtml { ... } is sugar for html({ ... }).

@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…