skip to content

Given fun build(block: StringBuilder.() -> Unit), what kinds of values can you pass as block? Can a regular extension function or a (StringBuilder) -> Unit lambda be passed?

level: seniorimportance: should knowfreq 40%

answer

  1. Receiver lambda, receiver-typed value, or extension ref all pass
  2. (A)->Unit value is NOT assignable to A.()->Unit
  3. References resolve against the expected type
  4. Adapt with { value(this) } or { a -> a.fn() }
  5. Call site allows a.f() and f(a)

basics

~20 s

You can pass a lambda written with the receiver style, or a reference to an extension function on StringBuilder. A plain (StringBuilder) -> Unit lambda is a different type, so it needs adapting, though references convert in many cases.

solid answer

~40 s

block expects type `StringBuilder.() -> Unit`. You can pass: (1) a receiver lambda literal `{ append("x") }`; (2) a reference to an **extension function** `StringBuilder::someExt` or a top-level `fun foo(): ... ` with matching receiver; (3) a value already typed as `StringBuilder.() -> Unit`. A value typed `(StringBuilder) -> Unit` is a **distinct type** and won't be accepted directly — Kotlin does not implicitly coerce parameter-function types to receiver-function types for stored values; you adapt with `{ paramLambda(this) }`. However, a **callable reference** to a member or regular function taking one StringBuilder (e.g. `::appendStuff` where `fun appendStuff(sb: StringBuilder)`) can satisfy a receiver type because references are resolved against the expected type. The receiver and first-parameter forms are assignment-incompatible as variables but interchangeable at the call site (`a.block()` vs `block(a)`).

code

kotlin · 12 lines
kotlin
fun build(block: StringBuilder.() -> Unit) = StringBuilder().apply(block).toString()

fun StringBuilder.ext() { append("E") }
fun plain(sb: StringBuilder) { sb.append("P") }

build { append("L") }          // receiver lambda
build(StringBuilder::ext)      // extension reference
build(::plain)                 // plain-fn reference resolved to receiver type

val pv: (StringBuilder) -> Unit = { it.append("V") }
// build(pv)                    // ERROR: stored (A)->Unit not assignable
build { pv(this) }             // adapt explicitly

go deeper

for a junior

Knows you pass a { } block to a builder param.

for a middle

Knows receiver lambdas and extension references both fit.

for a senior

Explains stored-value incompatibility vs reference resolution and writes the adapters.

for a principal

Reasons about API ergonomics: when to expose receiver vs parameter lambdas given interop and adaptation costs.

## What block accepts `block: StringBuilder.() -> Unit` is a **function type with receiver**. The compiler accepts any expression that conforms to that type: ```kotlin fun build(block: StringBuilder.() -> Unit): String = StringBuilder().apply(block).toString() // 1) receiver lambda literal — body sees StringBuilder as this build { append("hi") } // 2) a value already of the receiver type val r: StringBuilder.() -> Unit = { append("hi") } build(r) // 3) an extension function reference fun StringBuilder.exclaim() { append("!") } build(StringBuilder::exclaim) // 4) a reference to a plain function taking the receiver as its single param fun stuff(sb: StringBuilder) { sb.append("x") } build(::stuff) // reference is resolved against the expected receiver type ``` ## Receiver type vs parameter type as stored values For **stored variables**, `StringBuilder.() -> Unit` and `(StringBuilder) -> Unit` are **different, assignment-incompatible types**: ```kotlin val p: (StringBuilder) -> Unit = { it.append("x") } val q: StringBuilder.() -> Unit = p // ERROR: type mismatch val q2: StringBuilder.() -> Unit = { p(this) } // OK: adapt by calling p with this ``` Why: although both compile to a function with the same JVM erasure, Kotlin tracks the **receiver-ness** in the type system to drive `this`-resolution and DSL scoping. It will not silently convert one stored value to the other. ## Where they ARE interchangeable - **Call site:** for `f: A.() -> Unit` you may call `a.f()` or `f(a)` — both legal. - **Callable references:** a reference like `::stuff` (one param) or `Type::member` is resolved **against the expected type**, so it can fill either a receiver or parameter slot. This is why `build(::stuff)` works even though a stored `(StringBuilder)->Unit` value does not. ## Practical guidance When you have a `(A) -> Unit` value and an API wants `A.() -> Unit`, wrap it: `{ value(this) }`. When you have an `A.() -> Unit` and an API wants `(A) -> Unit`, wrap it: `{ a -> a.value() }`. ## Keywords/APIs - Function type with receiver, callable references (`::fn`, `Type::ext`). - `apply` to invoke `block` and return the builder. - Type compatibility / no implicit receiver-parameter coercion for stored values.

  • Why does build(::plain) compile but build(storedParamLambda) does not?
    A callable reference is resolved against the expected receiver type, while a stored (A)->Unit value carries the wrong static type and is not implicitly coerced.
  • How do you adapt a (StringBuilder) -> Unit value to fit StringBuilder.() -> Unit?
    Wrap it in a receiver lambda that forwards this: { value(this) }.

saying these in an interview costs you the question

  • Claims (A)->Unit and A.()->Unit are freely assignable
  • Says extension function references cannot be passed
  • Thinks the two types are completely unrelated and references never work
  • Cannot show the { value(this) } adapter

context