Sketch a minimal type-safe HTML builder so that html { body { p { +"Hello" } } } compiles and can render to a string. Identify the receiver lambdas, the child-attachment step, and the unaryPlus.
answer
- Tag base: name + children list + render()
- child(tag, block): attach then tag.block()
- Builder fns typed Child.() -> Unit returning the child
- operator fun String.unaryPlus() -> TextElement
- @DslMarker on base; html() seeds the tree
basics
~20 sMake a Tag class holding children. Each tag function creates a child, adds it to children, runs the block on it, and returns it. Add operator fun String.unaryPlus() to append text. A render() walks children to build the HTML string.
solid answer
~40 sDefine an abstract Tag with a name, a children list, a member operator fun String.unaryPlus() that appends a text node, and a protected helper to create-and-attach children. Concrete tags (HTML, BODY, P) expose builder functions typed name(block: Child.() -> Unit) that instantiate the child, add it to children, apply the block, and return it. The top-level fun html(block: HTML.() -> Unit) seeds the tree. render() recursively emits <name>...children...</name>, where a text child emits its escaped string. Annotate Tag with a @DslMarker to prevent receiver leakage. The three load-bearing pieces are: the HTML.() -> Unit receiver lambdas, the attach step (children += child) done before/around apply(block), and unaryPlus for text.
code
kotlin · 15 lines@DslMarker annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(private val n: String) {
private val kids = mutableListOf<() -> String>()
protected fun <T : Tag> child(t: T, b: T.() -> Unit): T { t.b(); kids += t::render; return t }
operator fun String.unaryPlus() { val s = this; kids += { s } }
fun render(): String = "<$n>" + kids.joinToString("") { it() } + "</$n>"
}
class HTML : Tag("html") { fun body(b: BODY.() -> Unit) = child(BODY(), b) }
class BODY : Tag("body") { fun p(b: P.() -> Unit) = child(P(), b) }
class P : Tag("p")
fun html(b: HTML.() -> Unit) = HTML().apply(b)
fun main() = println(html { body { p { +"Hello" } } }.render())go deeper
Can describe the pieces verbally — a Tag with children, tag functions, and +text — even if the code is rough.
Writes a compiling skeleton with receiver lambdas, the attach step, unaryPlus, and a render(); knows where the parent-child link forms.
Adds @DslMarker, escaping, attributes, and reasons about inline and the element type hierarchy.
Discusses API ergonomics, extensibility (custom tags), safety (escaping/injection), and how this scales to a full library like kotlinx.html.
## Design goals We want `html { body { p { +"Hello" } } }` to (a) compile with type-safe nesting, (b) only let each block configure its own node, and (c) render to a string. Three mechanisms do the work: 1. **Receiver lambdas** `Child.() -> Unit` for nesting. 2. **Attach-then-configure**: create child, add to `children`, run the block on it. 3. **`unaryPlus`** to append text children. ## Implementation ```kotlin @DslMarker annotation class HtmlTagMarker @HtmlTagMarker abstract class Tag(private val name: String) { private val children = mutableListOf<Element>() // attach-then-configure helper protected fun <T : Tag> child(tag: T, block: T.() -> Unit): T { children += tag tag.block() return tag } // text child via unary plus operator fun String.unaryPlus() { children += TextElement(this) } fun render(): String = "<$name>" + children.joinToString("") { it.render() } + "</$name>" } sealed interface Element { fun render(): String } class TextElement(private val text: String) : Element { override fun render() = text.replace("&", "&").replace("<", "<") } class HTML : Tag("html"), Element { fun body(block: BODY.() -> Unit) = child(BODY(), block) } class BODY : Tag("body"), Element { fun p(block: P.() -> Unit) = child(P(), block) } class P : Tag("p"), Element fun html(block: HTML.() -> Unit): HTML = HTML().apply(block) ``` Usage: ```kotlin val doc = html { body { p { +"Hello" } } } println(doc.render()) // <html><body><p>Hello</p></body></html> ``` ## Walking through the load-bearing parts - **`HTML.() -> Unit`** etc. — function types with receiver; inside each block `this` is the matching tag, enabling unqualified `body`/`p` calls. - **`child(tag, block)`** — the attach step: `children += tag` wires the node into the tree, then `tag.block()` configures it. Doing `apply(block)` after attaching is equivalent. - **`operator fun String.unaryPlus()`** — turns `+"Hello"` into a `TextElement` child; a member extension so it only works inside a tag. - **`@HtmlTagMarker`** — stops `p { }` from reaching an outer tag's builder by mistake. ## Extensions you might mention - Attributes via a `MutableMap<String,String>` and a setter or `attr("href", ...)`. - An `inline` modifier on the top-level builder to avoid the lambda allocation. - Escaping is required (`<`, `&`) to avoid producing broken/unsafe HTML — kotlinx.html does this. This ~30-line skeleton is the conceptual heart of kotlinx.html.
- Where exactly does the parent-child link get established?In the child() helper (or via children += tag): the new node is added to the current tag's children list before/while its configuration block runs.
- How would you add attributes like <a href=...>?Give Tag a MutableMap<String,String> for attributes and an attr() or property setter; render() then emits them inside the opening tag.
- Why mark the top-level html() inline?inline lets the compiler inline the receiver lambda, avoiding a function-object allocation per call — a minor perf win common in builder DSLs.
saying these in an interview costs you the question
- Forgetting to attach the child to the parent's list
- Using (Child) -> Unit instead of Child.() -> Unit so nesting breaks
- Omitting unaryPlus and trying to add text by return value
- No HTML escaping in render()
- Skipping @DslMarker and ignoring receiver leakage