skip to content

At the JVM level, how is a receiver function type A.() -> B represented, and what are the API-design tradeoffs of choosing a receiver lambda over a plain parameter lambda?

level: principalimportance: nice to knowfreq 30%

answer

  1. A.()->B == (A)->B == Function1 at runtime
  2. Receiver = first parameter on the JVM
  3. Difference is compile-time this binding only
  4. Receiver lambda: DSL/builder; plain: transform/callback
  5. From Java both are Function1

basics

~20 s

On the JVM a receiver lambda is the same as a plain one-arg function; the receiver is just the first argument. Choosing a receiver lambda makes APIs read like DSLs but can hide which object you're acting on and pollute scope.

solid answer

~50 s

A.() -> B compiles to the same functional interface as (A) -> B, i.e. kotlin.Function1<A, B>; the receiver is simply the first parameter (you can pass a (A)->B where an A.()->B is expected and vice versa via conversion). The only difference is compile-time: how this binds inside the body. Design tradeoffs: receiver lambdas (T.() -> R) give DSL-like, fluent call sites and unqualified member access, ideal for builders and configuration blocks; but they introduce implicit receivers that can shadow outer scopes, hurt discoverability, and require @DslMarker to stay safe. Plain lambdas ((T) -> R) are explicit (you name it), compose better with method references, and avoid scope pollution, at the cost of verbosity. From Java, a receiver function type is just a Function1, so the receiver-ness is invisible. For inline higher-order functions the choice is free of runtime cost. Prefer receiver lambdas for configuration/builder DSLs and plain lambdas for transformations and callbacks.

code

kotlin · 7 lines
kotlin
// Same underlying type; conversion both ways
val recv: Int.() -> Int = { this * 2 }
val plain: (Int) -> Int = recv      // OK: Function1<Int,Int>
val back: Int.() -> Int = plain     // OK as well

println(recv(21))   // 42 (receiver passed as arg)
println(5.recv())   // 10 (extension-style call)

go deeper

for a junior

Knows there's no runtime difference and that receiver gives this.

for a middle

Can state both are Function1 and that the receiver is the first parameter.

for a senior

Articulates the design tradeoffs (DSL fluency vs scope pollution/discoverability) and Java interop.

for a principal

Sets API-design policy: when to expose receiver vs plain lambdas across a codebase, factoring interop, composition, @DslMarker, and readability.

## JVM representation Kotlin function types are interfaces `kotlin.FunctionN`. A one-receiver-no-param lambda `A.() -> B` and a one-param lambda `(A) -> B` are **the same JVM type**: `Function1<A, B>`. The receiver is encoded as the **first parameter** of `invoke`. Consequences: - You can pass a `(A) -> B` where an `A.() -> B` is expected and the reverse, because they're convertible/identical at the type level. - From **Java**, both appear as `kotlin.jvm.functions.Function1`; the receiver semantics are a Kotlin-compiler-only notion. ```kotlin val r: StringBuilder.() -> Unit = { append("x") } val p: (StringBuilder) -> Unit = r // assignable val sb = StringBuilder(); r(sb); sb.r() // both invocation forms work ``` ## What actually differs Only **name resolution inside the body**: in `A.() -> B`, `this` is the `A` and its members are unqualified; in `(A) -> B` you reference the parameter (`it` or a named param). No runtime difference; with `inline` higher-order functions there's no allocation either way. ## Design tradeoffs **Choose a receiver lambda `T.() -> R` when:** - Building **DSLs / builders** (`html { }`, `buildString { }`) or **configuration blocks** (`apply`-style) where fluent, unqualified member access reads naturally. - The block predominantly *operates on* one object. **Costs of receiver lambdas:** - **Implicit receivers** can **shadow** outer `this`/receivers, causing confusing resolution — mitigate with `@DslMarker` and `this@Label`. - **Discoverability**: readers must know the receiver type to know what's callable. - **Composition**: harder to pass method references; `this`-based bodies don't compose like value-passing lambdas. **Choose a plain lambda `(T) -> R` when:** - Writing **transformations / callbacks** (`map`, `filter`, event handlers) where explicitness and `it`/named params aid clarity. - You want easy **method-reference** interop and no scope pollution. - The lambda may be consumed from **Java** or composed functionally. ## Practical guidance - Builders/configuration: receiver lambda + `@DslMarker`. - Data transforms and callbacks: plain lambda. - Library APIs exposed to Java: remember both are `Function1`; document intent. ## Key terms - **Function1**: the JVM interface backing single-argument Kotlin functions. - **Implicit receiver / shadowing**: the cost side of receiver lambdas. - **inline**: removes the runtime cost of either choice for higher-order functions.

  • Can you pass a (A) -> B literal where an A.() -> B is required?
    Yes; they share the JVM type Function1<A,B> and Kotlin permits the conversion. Only how this binds in a fresh literal body differs.
  • Does choosing a receiver lambda cost anything at runtime?
    No. It's the same Function1; with inline higher-order functions there's also no lambda allocation. The cost is design-level (scope clarity), not performance.

saying these in an interview costs you the question

  • Believing receiver lambdas are a distinct runtime type from plain ones
  • Claiming there's a performance penalty for receiver lambdas
  • Thinking Java sees a special receiver type (it sees Function1)
  • Recommending receiver lambdas universally without weighing scope pollution

context