skip to content

Implement a small type-safe builder DSL using a receiver function type (e.g. an html { } or buildString-style builder). Show the function signature and how the lambda gives the body an implicit receiver.

level: middleimportance: should knowfreq 60%

answer

  1. Param type: Builder.() -> Unit
  2. Glue: Builder().apply(init)
  3. Nested builders = member fns with receiver lambdas
  4. buildString is the stdlib example
  5. @DslMarker to scope receivers, inline to avoid alloc

basics

~20 s

Write a builder function that takes a lambda with the builder as its receiver (Builder.() -> Unit), create the builder, run the lambda on it, and return the result. Inside the block users call builder methods directly.

solid answer

~40 s

A type-safe builder is a function taking a receiver lambda: fun table(init: Table.() -> Unit): Table { val t = Table(); t.init(); return t }. The init: Table.() -> Unit parameter makes the block's this a Table, so callers invoke row(), cell() etc. unqualified. The pattern is usually expressed with apply: Table().apply(init). Nested builders repeat the pattern: a row(init: Row.() -> Unit) member adds a Row built the same way, giving a tree-shaped DSL. buildString { } is the stdlib example: inline fun buildString(builderAction: StringBuilder.() -> Unit): String. To prevent an inner block from accidentally seeing an outer builder's members, annotate the builder marker with @DslMarker so the compiler restricts implicit receivers. Making the builder fun inline avoids lambda allocation and enables non-local returns.

code

kotlin · 17 lines
kotlin
@DslMarker
annotation class MenuDsl

@MenuDsl
class Menu {
    private val items = mutableListOf<String>()
    fun item(name: String) { items += name }
    fun render() = items.joinToString(", ")
}

fun menu(init: Menu.() -> Unit): Menu = Menu().apply(init)

val m = menu {        // this: Menu
    item("New")
    item("Open")
}
println(m.render())   // New, Open

go deeper

for a junior

Can write a single-level builder fun f(init: B.() -> Unit) = B().apply(init).

for a middle

Builds nested builders correctly and explains why this is the relevant builder in each block.

for a senior

Adds @DslMarker and inline, and can explain receiver shadowing and non-local returns.

for a principal

Designs a coherent DSL surface (naming, marker scoping, error messages) and weighs DSL vs plain API maintainability.

## The recipe A **type-safe builder** turns nested object construction into readable, structured code. The core trick is a function whose parameter is a **receiver function type** `Builder.() -> Unit`. ```kotlin class Html { private val children = mutableListOf<String>() fun body(init: Body.() -> Unit) { children += Body().apply(init).render() } fun render() = "<html>${children.joinToString("")}</html>" } class Body { private val children = mutableListOf<String>() fun p(text: String) { children += "<p>$text</p>" } fun render() = "<body>${children.joinToString("")}</body>" } fun html(init: Html.() -> Unit): Html = Html().apply(init) ``` Usage reads like markup because each block's `this` is the relevant builder: ```kotlin val page = html { // this: Html body { // this: Body p("Hello") p("World") } } ``` ## Why it works - `init: Html.() -> Unit` is a **receiver lambda**, so inside `html { ... }` the implicit `this` is the `Html`, and `body` resolves on it. - `Html().apply(init)` runs the lambda against a fresh `Html` and returns it. `apply` itself is `T.() -> Unit` returning `T`, so it's the canonical glue. ## stdlib mirror `buildString` is the same shape: ```kotlin inline fun buildString(builderAction: StringBuilder.() -> Unit): String = StringBuilder().apply(builderAction).toString() ``` Similarly `buildList`/`buildMap` take `MutableList<T>.() -> Unit`. ## Hardening the DSL - **`@DslMarker`**: define `@DslMarker annotation class HtmlDsl` and put it on builder classes. The compiler then forbids calling an **outer** receiver's members from an inner block implicitly, preventing bugs like calling `body { }` inside another `body { }`. - **`inline`**: marking the builder `inline` removes lambda allocation and allows non-local `return`. ## Key terms - **Type-safe builder**: a DSL where the compiler checks structure via the type system. - **Implicit receiver**: the `this` injected by the receiver function type. - **@DslMarker**: an annotation that scopes implicit receivers to the nearest builder.

  • What does @DslMarker prevent?
    It stops an inner builder block from implicitly resolving calls against an enclosing builder's receiver, so you can't accidentally call outer-scope members; you'd need an explicit this@Outer to reach them.
  • Why mark the builder function inline?
    Inlining eliminates the lambda allocation and lets the block use non-local return; it's how buildString and buildList are defined.

saying these in an interview costs you the question

  • Using a plain (Builder) -> Unit and then complaining members aren't accessible
  • Forgetting to actually invoke init() on the builder
  • Not returning the built object
  • Being unaware of @DslMarker for nested-scope safety

context