skip to content

Write a generic curry/uncurry pair for two-argument functions in Kotlin and explain the closures and types involved. What are the runtime costs?

level: seniorimportance: should knowfreq 25%

answer

  1. curried(): (A)->(B)->R = { a -> { b -> this(a, b) } }
  2. uncurried(): (A,B)->R = { a, b -> this(a)(b) }
  3. Outer captures this; inner captures this + a
  4. Returned lambdas are real FunctionN objects — can't inline away
  5. Allocation + virtual invoke per stage; fine off hot paths

basics

~20 s

Write 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 s

A 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 lines
kotlin
fun <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)) // 5

go deeper

for a junior

Can write the nested-lambda curried form but may not articulate the allocation or type details.

for a middle

Implements both curried and uncurried correctly and explains which lambda captures what.

for a senior

Names FunctionN types, explains why these can't be inlined, and quantifies the allocation/dispatch cost versus a direct call.

for a principal

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))

context