DSL Building
🏗️ DSL Building
🎯 The Feature Behind Every Kotlin DSL: Lambda with Receiver
A normal lambda parameter type looks like (Int) -> String. A lambda with receiver looks like Int.() -> String — inside the lambda body, this refers to that receiver, exactly as if the lambda's code were written as a method on that type:
This is exactly how Kotlin's own apply, with, and run scope functions work — obj.apply { doThing() } reads naturally because the lambda passed to apply has obj as its receiver, so every member of obj is callable directly inside, without repeating obj. each time. DSL building is this same trick applied deliberately, on purpose-built types.
🧱 Building a Tiny HTML DSL, Step by Step
The goal: end up able to write something like this, and have it actually build a real HTML string:
Step 1 — a base Tag class that knows how to hold children and render itself:
Step 2 — specific tag classes and builder functions:
Every builder function (head { }, body { }, and html { } itself) takes a lambda with the relevant Tag subtype as its receiver. That's the entire mechanism: inside body { h1(...); p(...) }, h1 and p are directly callable because the lambda's implicit receiver is a Body, and h1/p are members of Body. No dot-prefixing, no explicit parameter name — it just reads like markup.
🔒 @DslMarker — Preventing Scope Leakage
Without any safeguard, an inner builder block can accidentally call a method from an outer receiver, since Kotlin allows reaching an outer implicit receiver when the inner one doesn't have a matching member:
@DslMarker is an annotation you define once, then apply to the DSL's base type — it tells the compiler to only allow the nearest matching implicit receiver, blocking accidental calls to an outer one:
With @HtmlDsl applied to Tag (and therefore inherited by Html, Head, Body, and everything else in this DSL), the compiler now enforces that inside body { }, only Body's own members are reachable — the invalid head { } call above becomes a genuine compile error instead of a silent trap.
Building an Object: Java Builder Pattern vs Kotlin DSL
| Java Builder Pattern | Kotlin DSL |
|---|---|
| new PageBuilder() .title("My Page") .addHeading("Welcome") .addParagraph("Hello!") .build(); | html { head { title("My Page") } body { h1("Welcome") p("Hello!") } } |
Both approaches build up a complex object step by step, but the DSL version expresses structure and nesting directly in the code's own shape — the indentation of head { } and body { } mirrors the actual document tree, something a flat chain of .method() calls can't represent at all.
💻 Coding Challenges
Challenge 1: A Lambda-with-Receiver Warm-Up
Write a function fun buildGreeting(init: StringBuilder.() -> Unit): String that creates a StringBuilder, calls init on it as a receiver, and returns .toString(). Use it to build a greeting by calling append(...) three times inside the lambda with no receiver prefix.
Goal: Get comfortable with the T.() -> Unit syntax before building a full DSL.
Challenge 2: A Simple Menu DSL
Build a small DSL for describing a restaurant menu: a Menu class holding a list of item strings with a function item(name: String, price: Double) that formats and stores "$name — $price", and a top-level fun menu(init: Menu.() -> Unit): Menu. Use it to build a 3-item menu and print each stored item.
Goal: Build a minimal DSL from scratch, following the html{} pattern from the chapter.
Challenge 3: Extending the HTML DSL
Using the chapter's Tag/Html/Body DSL as a starting point, add a new tag type ul (unordered list) to Body, which accepts a lambda that can call li(text: String) one or more times for list items. Build a small page using body { ul { li("First") ; li("Second") } } and print the rendered result.
Goal: Extend an existing DSL with a new nested builder, following the established pattern.
Every time apply { }, with(x) { }, or a Gradle dependencies { } block has been written or read, it's been this exact mechanism in action. Building a DSL from scratch here isn't introducing something new so much as revealing the machinery behind patterns that already felt natural.
🎯 What's Next
Next chapter: Reflection & Annotations — KClass, the reflection API, custom annotations, and use-site targets.