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?
answer
- A.()->B == (A)->B == Function1 at runtime
- Receiver = first parameter on the JVM
- Difference is compile-time this binding only
- Receiver lambda: DSL/builder; plain: transform/callback
- From Java both are Function1
basics
~20 sOn 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 sA.() -> 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// 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
Knows there's no runtime difference and that receiver gives this.
Can state both are Function1 and that the receiver is the first parameter.
Articulates the design tradeoffs (DSL fluency vs scope pollution/discoverability) and Java interop.
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