Write a generic curry/uncurry pair for two-argument functions in Kotlin and explain the closures and types involved. What are the runtime costs?
answer
- curried(): (A)->(B)->R = { a -> { b -> this(a, b) } }
- uncurried(): (A,B)->R = { a, b -> this(a)(b) }
- Outer captures this; inner captures this + a
- Returned lambdas are real FunctionN objects — can't inline away
- Allocation + virtual invoke per stage; fine off hot paths
basics
~20 sWrite an extension that turns an (A, B) -> R into (A) -> (B) -> R by returning nested lambdas, and an uncurry that does the reverse. Each curried stage creates a small function object, so there's a tiny allocation cost.
solid answer
~50 sA generic `curry` for arity-2 is `fun <A, B, R> ((A, B) -> R).curried(): (A) -> (B) -> R = { a -> { b -> this(a, b) } }`, and the inverse `fun <A, B, R> ((A) -> (B) -> R).uncurried(): (A, B) -> R = { a, b -> this(a)(b) }`. The outer lambda captures `this` (the original function); the inner lambda additionally captures `a` — two closures. Types: `(A, B) -> R` is `Function2`, `(A) -> (B) -> R` is `Function1` returning `Function1`. Runtime cost: each *partial* call (`curried()(a)`) allocates a new inner closure object capturing `a`; the chain therefore allocates more than a single direct call. These extension functions are *not* `inline` here because they return function values, so the lambdas become real objects on the heap. For hot paths prefer direct calls or default arguments; reserve currying for combinator-style code where the abstraction pays for itself.
code
kotlin · 6 linesfun <A, B, R> ((A, B) -> R).curried(): (A) -> (B) -> R = { a -> { b -> this(a, b) } }
fun <A, B, R> ((A) -> (B) -> R).uncurried(): (A, B) -> R = { a, b -> this(a)(b) }
val add: (Int, Int) -> Int = { x, y -> x + y }
println(add.curried()(2)(3)) // 5
println(add.curried().uncurried()(2, 3)) // 5go deeper
Can write the nested-lambda curried form but may not articulate the allocation or type details.
Implements both curried and uncurried correctly and explains which lambda captures what.
Names FunctionN types, explains why these can't be inlined, and quantifies the allocation/dispatch cost versus a direct call.
Weighs the abstraction against hot-path allocation budgets and team readability, recommending defaults/direct calls by default and combinators only where they earn it.
## Goal Provide reusable `curried()` / `uncurried()` extensions for two-argument functions, and understand exactly what they cost at runtime. ## The implementations ```kotlin // (A, B) -> R ==> (A) -> (B) -> R fun <A, B, R> ((A, B) -> R).curried(): (A) -> (B) -> R = { a -> { b -> this(a, b) } } // (A) -> (B) -> R ==> (A, B) -> R fun <A, B, R> ((A) -> (B) -> R).uncurried(): (A, B) -> R = { a, b -> this(a)(b) } val add: (Int, Int) -> Int = { x, y -> x + y } val cadd = add.curried() // (Int) -> (Int) -> Int cadd(2)(3) // 5 val back = cadd.uncurried() // (Int, Int) -> Int back(2, 3) // 5 ``` ## The closures, step by step In `curried()`: - The **outer** lambda `{ a -> ... }` captures `this` — the receiver function — so it is a closure over one variable. - The **inner** lambda `{ b -> this(a, b) }` captures both `this` and `a`. It's created *anew each time* the outer lambda runs (i.e., each time you supply `a`). So `add.curried()` makes one object (the outer function); `add.curried()(2)` makes a second object (the inner function capturing `a = 2`). ## The types under the hood Kotlin function types compile to the `FunctionN` interfaces from the standard library: - `(A, B) -> R` is `Function2<A, B, R>`. - `(A) -> (B) -> R` is `Function1<A, Function1<B, R>>`. Each lambda is an instance implementing the corresponding `invoke`. ## Runtime costs - **Allocations.** A direct `add(2, 3)` is a single `invoke` call. The curried chain allocates the outer closure plus a fresh inner closure per `a`. In tight loops this matters. - **No inlining.** These extensions **cannot meaningfully be `inline`**, because they *return* a lambda as a value — `inline` erases lambdas only when they are *called* within the inline body, not when handed back to the caller. So the function objects are real heap allocations. (Marking it `inline` would force `noinline` on the returned lambda and gain nothing.) - **Indirection.** Each stage is a virtual `invoke` dispatch. ## Practical guidance Use currying for **combinator/DSL** code where composing single-argument functions improves clarity (`compose`, point-free pipelines). On hot paths, or for simple "fix-an-argument" needs, prefer **default arguments** or a direct closure. Always measure before assuming the cost matters — for most application code the allocations are negligible.
- Why can't you make curried() inline to remove the allocations?inline only eliminates a lambda's object when it is invoked inside the inline body. curried() returns the lambda to the caller, so it must exist as a real object; the compiler would force noinline on it. The allocation stays.
- What concrete standard-library type does (A) -> (B) -> R compile to?Function1<A, Function1<B, R>> — a Function1 whose result is itself a Function1.
saying these in an interview costs you the question
- Claiming inline removes the returned-lambda allocation
- Forgetting the inner lambda is recreated per call to the outer one
- Saying currying has zero runtime overhead versus a direct call
- Getting the uncurried type/implementation wrong (e.g., this(a, b) instead of this(a)(b))