skip to content

What single Kotlin language feature makes type-safe internal DSLs (like the kotlinx.html `html { body { ... } }` builder) possible, and what does it do?

level: juniorimportance: must knowfreq 70%

answer

  1. A.() -> Unit = lambda with receiver
  2. Implicit this = the receiver instance
  3. buildString / apply / with use it
  4. Trailing lambda makes it read declaratively
  5. Each nested block = new receiver

basics

~20 s

A lambda with a receiver. The lambda type names a type before the dot, so inside the lambda that type becomes 'this'. You can call its methods directly, which makes nested config blocks read like a mini language.

solid answer

~40 s

The key feature is the function type with a receiver, written `A.() -> Unit`. When a function takes such a lambda, the block runs with an instance of `A` as the implicit `this` (the receiver). Inside the block you call `A`'s members and extensions without any qualifier, so `html { body { p { +"hi" } } }` is just nested calls each scoped to a different receiver. This is what people mean by an internal DSL: it's plain Kotlin, but receiver lambdas plus infix functions, operator overloads (like `unaryPlus`), and trailing-lambda syntax make it read declaratively. Standard-library examples are `apply`, `with`, and `buildString`, all of which use receiver lambdas.

code

kotlin · 7 lines
kotlin
fun buildString(builder: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builder).toString()

val greeting = buildString {
    append("Hello, ")   // 'this' is StringBuilder
    append("DSL!")
}

go deeper

for a junior

Knows the syntax A.() -> Unit and that inside the block this is the receiver, can read a builder.

for a middle

Can write a small builder function that invokes a receiver lambda and explain trailing-lambda sugar.

for a senior

Explains nesting (each block = new receiver) and the supporting operators (unaryPlus, infix) that make it declarative.

for a principal

Frames internal vs external DSLs, discusses when a DSL pays off versus a plain config object, and notes @DslMarker for scope safety.

## The core idea An **internal DSL** is a domain-specific 'language' that is actually just normal library code in the host language (here Kotlin). Nothing is parsed specially — the compiler sees ordinary function calls — but the code *reads* like a declarative configuration language. The one feature that makes this possible in Kotlin is the **lambda with receiver**, also called a **function type with receiver**. ## Function type with receiver: `A.() -> Unit` A normal lambda type is `(Int) -> String`. A **receiver** type adds a type before the dot: ```kotlin val block: StringBuilder.() -> Unit = { append("hi") } ``` Here `block` is a lambda that, when invoked, runs *as if it were a method on a `StringBuilder`*. The `StringBuilder` instance becomes the **implicit `this`** (the receiver) inside the lambda body, so you can call `append(...)` directly — no `sb.append`. ## How a builder function uses it A builder function declares a parameter of this type and invokes the lambda on an object it created: ```kotlin fun buildString(builderAction: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.builderAction() // run the lambda with sb as receiver return sb.toString() } val s = buildString { // trailing-lambda syntax append("Hello, ") // 'this' is the StringBuilder append("world") } ``` Because the lambda is the last argument, Kotlin lets you write it as a trailing `{ ... }` block. Combined with the implicit receiver this looks like a configuration language. ## Why nesting works Each nested builder takes its *own* receiver lambda, so each `{ }` introduces a new implicit `this`: ```kotlin html { // this: HTML body { // this: BODY p { // this: P +"text" // unaryPlus operator on P adds a text node } } } ``` ## Other features that polish the DSL - **`operator fun unaryPlus()`** — lets `+"text"` mean 'add this text'. - **`infix fun`** — lets `"a" to 1` or `width equalTo 10` read like English. - **Trailing lambdas + default args** — keep call sites clean. - **`@DslMarker`** — restricts implicit receivers so you can't accidentally call an outer builder from an inner scope (covered in builder-pattern questions). The takeaway: the *enabling* feature is the receiver lambda `A.() -> Unit`; everything else is sugar on top.

  • How is `A.() -> Unit` different from `(A) -> Unit`?
    Both can be called with an `A`, but with the receiver type `A` is the implicit `this` (call members unqualified); with the parameter type `A` is an explicit named argument (e.g. `it`). The receiver form is what enables the DSL look.
  • Name a standard-library function built on a receiver lambda.
    `apply` (`T.(T.() -> Unit): T`), `with`, `run`, and `buildString` / `buildList` all use receiver lambdas.

It's like handing someone a clipboard already clipped to a specific form — they just start filling fields ('this' form) without re-stating which form each time.

saying these in an interview costs you the question

  • Claiming Kotlin parses DSLs specially or has a 'dsl' keyword
  • Confusing `A.() -> Unit` with `(A) -> Unit`
  • Saying the receiver is passed as `it`
  • Not being able to name any concrete builder (apply/buildString)

context