Is `xs.map(::process)` ALWAYS substitutable for `xs.map { process(it) }`? Give concrete cases where a function reference cannot replace the lambda, or behaves differently.
answer
- Reference == lambda only for a bare forward
- Transform/capture/reorder/default -> need a lambda
- Arity must match; references fix full param list
- Inline funcs inline lambdas, not references
- Vararg/named/suspend not expressible via `::`
basics
~20 sNo. A reference only works when you forward the argument straight through, unchanged. If you need to transform the input, use defaults, reorder or drop parameters, add captured values, or handle overloads the compiler can't pick, you must use a lambda.
solid answer
~50 s`::process` and `{ process(it) }` coincide only for a **pure forward** of a single argument. They diverge when: (1) you must **transform** the argument (`{ process(it.trim()) }`); (2) the target relies on **default arguments** you want to omit — a reference fixes the full arity; (3) you need to **reorder/drop** parameters or supply **extra captured** values; (4) **overload resolution** can't disambiguate the reference from the expected type alone but the lambda's call can; (5) **arity mismatch** — `process(a, b)` can't be `::process` for a `(T) -> R` slot, but `{ process(it, fixed) }` works; (6) **vararg / named args** aren't expressible via a reference; (7) **suspend** mismatch. Also, with **inlined** higher-order functions, a lambda is inlined (no object allocation) whereas a reference may materialize a function object — a subtle perf/allocation difference. Behaviorally for the forward case they're identical.
code
kotlin · 9 linesfun scale(x: Int, factor: Int = 1) = x * factor
val xs = listOf(1, 2, 3)
val factor = 10
xs.map { scale(it, factor) } // captures factor -> [10,20,30]
// xs.map(::scale) // ERROR: ::scale is (Int,Int)->Int, not (Int)->Int
xs.map { scale(it) } // uses default factor=1 -> [1,2,3]
// xs.map(::scale) // still wrong arity for the default casego deeper
Knows references only fit the simple forward case and that transforms need a lambda.
Lists arity, default-argument, and capture mismatches as concrete blockers.
Adds overload-resolution and suspend nuances and reasons about substitutability precisely.
Weighs the inline/allocation tradeoff and sets guidance for hot paths and public API ergonomics.
## The substitution rule `xs.map(::process)` equals `xs.map { process(it) }` **only when the lambda is a bare, single-argument forward** to `process`. Outside that, references can't express what the lambda can. ## Cases where the reference cannot replace the lambda ### 1. You transform the argument ```kotlin xs.map { process(it.trim()) } // cannot be ::process ``` A reference forwards the element unchanged; any pre-processing requires a lambda. ### 2. Default arguments you want to omit ```kotlin fun process(x: String, verbose: Boolean = false) = ... xs.map { process(it) } // uses default verbose=false // xs.map(::process) // ERROR: ::process has arity 2 here ``` A reference fixes the **full parameter list**; it can't drop a defaulted parameter. ### 3. Reorder, drop, or add captured values ```kotlin val factor = 3 xs.map { scale(it, factor) } // captures factor; ::scale can't xs.map { (a, b) -> combine(b, a) } // reorder; not expressible by a reference ``` ### 4. Overloads the expected type can't disambiguate When two overloads match the expected function type, `::process` stays ambiguous, but `{ process(it) }` resolves via normal call-site overload resolution. ### 5. Arity mismatch ```kotlin fun process(a: T, b: T): R xs.map { process(it, default) } // OK // xs.map(::process) // ERROR: needs (T) -> R, got (T,T) -> R ``` ### 6. Vararg / named arguments A bare reference can't spread a `vararg` or use named arguments; wrap in a lambda. ### 7. suspend mismatch A reference to a non-`suspend` function does not satisfy a `suspend (T) -> R` slot; a `suspend` lambda or a reference to a suspend function is needed. ## Subtle performance note: inlining Many standard higher-order functions (`map`, `filter`, `forEach`) are **inline**. A **lambda** passed to an inline function is **inlined** at the call site — no function object is allocated. A **function reference** is generally **not** inlined the same way; it may materialize a `FunctionN` object (top-level references can be cached as singletons, reducing this). So in hot loops the lambda can avoid an allocation the reference incurs — usually negligible, but worth knowing. ```kotlin inline fun <T> List<T>.each(action: (T) -> Unit) { for (e in this) action(e) } list.each { println(it) } // lambda body inlined, no object list.each(::println) // may use a function-object instance ``` ## Decision guide - Pure single-arg forward, no overload ambiguity -> reference is cleaner. - Any transform/capture/reorder/default/vararg/suspend mismatch -> lambda. - Hot path inside an inline function where allocation matters -> prefer the lambda.
- Why can't `::process` use a default argument the way a lambda can?A reference binds to the function's full declared signature/arity. Defaults are applied at a call expression; a reference is not a call, so it can't omit parameters.
- Does choosing a reference over a lambda ever change allocation behavior?Yes. With inline higher-order functions the lambda body is inlined (no object), while a reference may allocate a FunctionN instance, though top-level references can be singletons.
saying these in an interview costs you the question
- Insisting references and lambdas are always interchangeable
- Not recognizing arity/default mismatches
- Ignoring overload-resolution differences
- Unaware that inline functions inline lambdas but not references
- Claiming references can spread varargs or use named args