Design a minimal type-safe builder DSL for an HTML-like structure (`html { head { title { +"x" } } body { p { +"y" } } }`). Walk through how receiver lambdas, nesting, and node collection work.
answer
- Base Tag with children + initTag helper
- initTag: build child, child.init(), add, return
- Typed methods per tag enforce grammar
- @DslMarker stops outer-receiver leaks
- html { } bootstraps via apply
basics
~20 sMake a class per tag holding children. Each tag has methods that take a receiver lambda, create a child, run the lambda on it, add it to the children, and return it. A top-level function starts the tree. Text uses a unaryPlus operator.
solid answer
~40 sModel each element as a class (a `Tag` base with a `children: MutableList<Tag>` and a `render`). Give the base a generic helper that takes a `T.() -> Unit`: it constructs the child `T`, applies the receiver lambda (`child.init()`), adds it to `children`, and returns it. Concrete tags (`HTML`, `BODY`, `P`) expose named functions delegating to that helper: `fun body(init: BODY.() -> Unit) = initTag(BODY(), init)`. A free function `html { ... }` bootstraps the root and applies its lambda. Text content is added via `operator fun String.unaryPlus()` on tags. Each `{ }` opens a new receiver scope, so nesting is automatic. Add a `@DslMarker` annotation (e.g. `@HtmlTagMarker`) on the tag types so an inner `p { }` can't accidentally call an outer `body { }`. This is exactly how kotlinx.html is structured.
code
kotlin · 13 lines@DslMarker annotation class TagMarker
@TagMarker abstract class Tag(val name: String) {
val children = mutableListOf<Tag>()
protected fun <T : Tag> init(c: T, b: T.() -> Unit): T { c.b(); children += c; return c }
operator fun String.unaryPlus() { children += Text(this) }
}
class Text(val v: String) : Tag("#text")
class HTML : Tag("html") { fun body(b: BODY.() -> Unit) = init(BODY(), b) }
class BODY : Tag("body") { fun p(b: P.() -> Unit) = init(P(), b) }
class P : Tag("p")
fun html(b: HTML.() -> Unit) = HTML().apply(b)go deeper
Can read the DSL and understand +"text" but would struggle to design the builder helper.
Implements the per-tag builder methods and the unaryPlus, gets nesting working.
Designs the generic initTag, enforces grammar via typed methods, and adds @DslMarker for scope safety.
Considers rendering strategy, immutability, performance, and how the design scales to a real markup library like kotlinx.html.
## Goal Build a small DSL that produces a tree from declarative code, type-safely (you can only put `<p>` where the API allows it). ## Step 1 — a base class that collects children ```kotlin @DslMarker annotation class HtmlTagMarker @HtmlTagMarker abstract class Tag(val name: String) { val children = mutableListOf<Tag>() val text = StringBuilder() // the reusable builder helper protected fun <T : Tag> initTag(child: T, init: T.() -> Unit): T { child.init() // run the receiver lambda with child as this children += child return child } operator fun String.unaryPlus() { text.append(this) } fun render(sb: StringBuilder) { sb.append("<").append(name).append(">").append(text) children.forEach { it.render(sb) } sb.append("</").append(name).append(">") } } ``` The heart is `initTag`: it (1) takes the already-built child and a receiver lambda, (2) invokes that lambda on the child so nested calls configure *it*, (3) records the child, (4) returns it. ## Step 2 — concrete tags expose typed entry points ```kotlin class HTML : Tag("html") { fun head(init: HEAD.() -> Unit) = initTag(HEAD(), init) fun body(init: BODY.() -> Unit) = initTag(BODY(), init) } class HEAD : Tag("head") { fun title(init: TITLE.() -> Unit) = initTag(TITLE(), init) } class TITLE : Tag("title") class BODY : Tag("body") { fun p(init: P.() -> Unit) = initTag(P(), init) } class P : Tag("p") ``` Because `body` is only declared on `HTML`, you *cannot* write `head { body { } }` — the type system enforces the grammar. That's the 'type-safe' in type-safe builder. ## Step 3 — bootstrap function ```kotlin fun html(init: HTML.() -> Unit): HTML = HTML().apply(init) ``` ## Step 4 — use it ```kotlin val doc = html { head { title { +"Hello" } } body { p { +"World" } } } val sb = StringBuilder(); doc.render(sb) // <html><head><title>Hello</title></head><body><p>World</p></body></html> ``` ## Why each piece matters - **Receiver lambda `T.() -> Unit`** — gives each block its own implicit `this`, so calls are unqualified. - **Nesting** — falls out naturally because `initTag` runs the child's lambda before returning; deeper blocks just call deeper builders. - **`unaryPlus`** — turns `+"text"` into content. - **`@DslMarker` (`@HtmlTagMarker`)** — without it, inside `title { }` you could still call `body { }` (an outer-receiver member) by implicit resolution; the marker forbids that, requiring explicit `[email protected] { }` if you really mean it. - **Return value `T`** — lets you keep a handle or chain. ## Performance / correctness notes - The lambdas are typically `inline`d by the standard helpers (`apply`), so there's little overhead. - Building eagerly into mutable nodes is simple; for large trees you might stream rendering instead.
- How does the type system stop you from putting a `<p>` directly under `<head>`?The `p(...)` builder method is declared only on `BODY` (and wherever it's valid), not on `HEAD`. Since the receiver inside `head { }` is `HEAD`, `p { }` simply doesn't resolve — a compile error.
- Why add `@DslMarker` to the Tag base?It marks all tag receivers as the same DSL scope so that, when receivers nest, an inner block cannot *implicitly* call a member of an outer receiver; you'd have to qualify with `this@html`. This catches accidental cross-scope calls at compile time.
saying these in an interview costs you the question
- Storing children but never invoking the child's lambda
- Putting all tag methods on one type, losing type safety
- Forgetting @DslMarker and allowing outer-receiver leakage
- Returning Unit so the builder can't be chained or captured
- Re-creating the child after configuring it (losing the config)