skip to content

Compare `fun config(block: Server.() -> Unit)` with `fun config(block: (Server) -> Unit)`. How does each change the call site, and why does the DSL community prefer the first?

level: middleimportance: must knowfreq 60%

answer

  1. Receiver = implicit this, param = it.
  2. Server().apply(block) bridges to receiver form
  3. Nesting amplifies the readability win
  4. @DslMarker guards against outer-receiver leakage
  5. Param form better for plain callbacks

basics

~20 s

With the receiver version you write member calls directly inside the block. With the parameter version you must prefix every call with the lambda argument, usually it. The receiver version reads cleaner, so DSLs use it.

solid answer

~40 s

`Server.() -> Unit` is a function type *with receiver*: the lambda runs with a `Server` as the implicit `this`, so the call site is `config { port = 8080; ssl = true }`. `(Server) -> Unit` is an ordinary single-parameter lambda: the `Server` arrives as `it` (or a named param), so you must qualify everything: `config { it.port = 8080; it.ssl = true }`. Both compile to similar bytecode (an instance plus a function), and inside the receiver version you can still access an explicit `this`. DSLs prefer the receiver form because removing the `it.` qualifier is exactly what makes nested builder blocks read like a declarative language. The receiver form also composes: nested builders each introduce their own `this`, and `@DslMarker` can then constrain which receivers are implicitly reachable.

code

kotlin · 7 lines
kotlin
class Router { fun get(path: String, h: () -> Unit) {} }

fun routing(block: Router.() -> Unit) = Router().apply(block)

routing {                 // this: Router
    get("/health") { }    // no qualifier needed
}

go deeper

for a junior

Knows the call site differs by needing it. in the parameter form.

for a middle

Writes both builder functions and explains the apply bridge and why DSLs nest with the receiver form.

for a senior

Discusses member-resolution semantics, explicit this, and the @DslMarker scope hazard at depth.

for a principal

Weighs when each form is appropriate API design, considers readability vs. explicitness trade-offs and library-wide consistency.

## Two lambda shapes Kotlin has two ways to pass an object into a lambda: | Type | The object is… | Call site | |------|----------------|-----------| | `Server.() -> Unit` (receiver) | the implicit `this` | `config { port = 8080 }` | | `(Server) -> Unit` (parameter) | the argument `it` / named param | `config { it.port = 8080 }` | ## The builder side ```kotlin class Server { var port = 80; var ssl = false } // receiver form fun receiverConfig(block: Server.() -> Unit): Server = Server().apply(block) // invokes block with the Server as receiver // parameter form fun paramConfig(block: (Server) -> Unit): Server { val s = Server(); block(s); return s } ``` Note `apply` itself *is* a receiver-lambda function, which is why `Server().apply(block)` works when `block` is `Server.() -> Unit`. ## The call site difference ```kotlin receiverConfig { port = 8080 // this.port, 'this' is implicit ssl = true } paramConfig { it.port = 8080 // must qualify with it it.ssl = true } ``` The receiver form removes the noise. For a *single* `apply`-style block the difference is small, but DSLs nest deeply, and at depth the `it.` qualifiers (or worse, `outerIt.inner.x`) destroy readability. ## Semantics & interop - A `Server.() -> Unit` value can be **called as a method**: `server.block()`. A `(Server) -> Unit` value is called as `block(server)`. - The receiver is essentially an extra first parameter under the hood, so the JVM representation is very similar; the difference is how member resolution works inside the body. - Inside a receiver lambda you can still write `this` explicitly (e.g., to pass the whole object), and you can shadow outer receivers — which is why **`@DslMarker`** exists: it stops an inner block from implicitly resolving a *member of an outer receiver* by accident. ## When to choose which - **Receiver** for builders/config DSLs (kotlinx.html, Ktor routing, Gradle Kotlin DSL, `buildList`). - **Parameter** when the object is just one input among several, or when an explicit name reads better (e.g. callbacks like `forEach { item -> ... }`).

  • Can you call a `Server.() -> Unit` lambda the same way as a `(Server) -> Unit`?
    Effectively yes — `server.block()` and `block(server)` both work because the receiver is just an extra first parameter under the hood; the difference is purely how members resolve inside the body.
  • Why might `@DslMarker` matter once you nest receiver lambdas?
    Without it, an inner block can implicitly see and call members of an outer receiver, producing confusing or wrong DSLs. `@DslMarker` makes the outer implicit receiver inaccessible unless explicitly qualified.

saying these in an interview costs you the question

  • Saying both produce identical call sites
  • Believing the receiver form is slower or boxes more
  • Thinking you cannot use explicit `this` in a receiver lambda
  • Recommending the receiver form for ordinary callbacks where a name reads better

context