Skip to content

02 · Lambdas & Higher-Order Functions

A lambda is a function literal — a block of code you can pass around as a value. A higher-order function is any function that takes another function as a parameter or returns one. Together these are the foundation of Kotlin's functional style, and they're what makes the collection operations in Module 5 (map, filter, reduce) so compact.

Lambda syntax and function types

A lambda is written { parameters -> body }. Its type is a function type, written (ParamTypes) -> ReturnType.

fun main() {
    val square: (Int) -> Int = { x -> x * x }
    println(square(5))          // 25

    val add: (Int, Int) -> Int = { a, b -> a + b }
    println(add(2, 3))           // 5

    val sayHi: () -> Unit = { println("Hi!") }
    sayHi()
}
25
5
Hi!

The compiler can infer the lambda's parameter types from context, so you rarely need to spell out (Int) -> Int explicitly when passing a lambda directly to a function that expects one — Kotlin figures it out from the parameter's declared type.

Higher-order functions: taking a function as a parameter

A function is "higher-order" once one of its parameters is itself a function type.

fun applyTwice(x: Int, operation: (Int) -> Int): Int {
    return operation(operation(x))
}

fun main() {
    val result = applyTwice(3) { n -> n * 2 }   // trailing lambda syntax
    println(result)   // 12 -- (3*2)*2
}
12

When the last parameter of a function is a function type, Kotlin lets you move the lambda outside the parentheses — that's the "trailing lambda" syntax used above (applyTwice(3) { n -> n * 2 } instead of applyTwice(3, { n -> n * 2 })). If the lambda is the only argument, the parentheses can be dropped entirely.

The implicit it

When a lambda takes exactly one parameter and you don't name it, Kotlin lets you refer to it as it:

fun describe(numbers: List<Int>, predicate: (Int) -> Boolean): List<Int> =
    numbers.filter(predicate)

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    println(describe(numbers) { it % 2 == 0 })   // [2, 4, 6] -- `it` is each number
}
[2, 4, 6]

it is convenient for short lambdas but hurts readability once the lambda does anything non-trivial — name the parameter explicitly ({ n -> ... }) as soon as the body is more than one simple expression.

Functions that return functions

A function can also return a function type, which is how you build configurable behavior — a function factory.

fun multiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }
}

fun main() {
    val double = multiplier(2)
    val triple = multiplier(3)

    println(double(5))   // 10
    println(triple(5))   // 15
}
10
15

Closures: capturing variables

A lambda "closes over" (captures) variables from its surrounding scope, including mutable ones — unlike Java's lambdas, which can only capture effectively-final variables.

fun counter(): () -> Int {
    var count = 0
    return {
        count++          // mutating a captured `var` -- allowed in Kotlin
        count
    }
}

fun main() {
    val next = counter()
    println(next())   // 1
    println(next())   // 2
    println(next())   // 3 -- `count` persists between calls, tied to this closure instance
}
1
2
3

Each closure captures its own copy of the enclosing state

Calling counter() again creates a brand-new count starting at 0 — closures are independent per call. A classic trap is capturing a loop variable expecting a fresh value per iteration:

fun main() {
    val actions = mutableListOf<() -> Unit>()
    for (i in 1..3) {
        actions.add { println(i) }   // captures `i` by value at each iteration in Kotlin's for-loop
    }
    actions.forEach { it() }   // 1, 2, 3 -- Kotlin's for-loop gives each iteration its own `i`
}

Kotlin's for (i in range) binds a fresh i each iteration, so this prints 1, 2, 3 as expected. The equivalent trap in some other languages (a shared mutable loop variable) doesn't apply here — but if you capture a var you mutate after the loop, every closure still sees the final value, since they all reference the same variable.

Function references

Instead of wrapping an existing function in a lambda, reference it directly with ::.

fun isEven(n: Int) = n % 2 == 0

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    val words = listOf("", "hi", "", "kotlin")

    // Equivalent ways to pass the same behavior:
    println(numbers.filter { isEven(it) })   // lambda wrapping the function
    println(numbers.filter(::isEven))         // function reference -- more direct

    // Member references (Type::method) work the same way for instance methods:
    println(words.filter(String::isNotEmpty))
}
[2, 4, 6]
[2, 4, 6]
[hi, kotlin]

Why inline matters

Every lambda is, under the hood, an object implementing a function interface — passing one normally means allocating an object and an indirect call. For small, hot higher-order functions (like the custom ones above), Kotlin lets you mark the function inline: the compiler copies the function's and the lambda's code directly into the call site, eliminating both the object allocation and the call overhead.

inline fun measureAndRun(label: String, block: () -> Unit) {
    val start = System.nanoTime()
    block()
    val elapsedMs = (System.nanoTime() - start) / 1_000_000.0
    println("$label took ${elapsedMs}ms")
}

fun main() {
    measureAndRun("sum") {
        var total = 0L                     // Long -- the sum overflows Int past ~46,000 terms
        for (i in 1..1_000_000) total += i
        println("Total: $total")
    }
}
Total: 500000500000
sum took 3.1ms   // exact timing varies per run

inline is also what makes return from inside a lambda passed to that function work like a normal early return from the enclosing function (a "non-local return") — without inline, that return wouldn't be legal. Almost all of Kotlin's standard library higher-order functions (filter, map, let, run, also, apply) are inline for exactly this reason.

How It Actually Works

The JVM (pre-invokedynamic-lambdas era, and still by default for Kotlin) has no native "function value" type, so every non-inline lambda has to become a real object implementing a real interface. Kotlin represents a function type (Int) -> Int as the generic interface kotlin.jvm.functions.Function1<Int, Int>, with a single abstract method invoke(p1: P1): R. A lambda literal like { x -> x * x } compiles to an anonymous class implementing Function1, whose invoke() body is your lambda's code; square(5) then compiles to square.invoke(5). Function types with different arities map to Function0, Function2, Function3, ... up to Function22 — which is also why Kotlin lambdas historically topped out around 22 parameters.

This matters for performance: each time a non-inline lambda literal is created, the compiler either allocates a new anonymous-class instance on the spot, or — if the lambda captures no variables from its enclosing scope (no closure) — reuses a single cached singleton instance of that anonymous class, since a captureless lambda is stateless and safe to share. A lambda that does capture a variable (a "closure") gets fields added to its generated anonymous class to hold the captured values, copied in via its generated constructor at the point the lambda literal is evaluated — which is why a var captured by a lambda in Kotlin is wrapped in a Ref.IntRef-style holder object internally, so multiple lambdas capturing and mutating the same outer var all see the same box rather than independent copies.

The it shorthand is resolved entirely at compile time by name-lookup convention — the compiler simply binds the single implicit parameter to the identifier it while type-checking the lambda body; there's no runtime concept of "it" at all, it never appears in the generated invoke() method's bytecode as anything other than parameter slot 1.

Cheat sheet

Syntax Meaning
(Int) -> String Function type: takes an Int, returns a String
{ x -> x * 2 } Lambda literal with named parameter
{ it * 2 } Lambda with implicit single parameter
f(x) { ... } Trailing lambda -- last function-type param moves outside ()
::functionName Reference to a top-level or member function
inline fun f(block: () -> Unit) Lambda body copied inline -- no allocation, allows non-local return

Exercise

Write a higher-order function retry(times: Int, action: () -> Boolean): Boolean that calls action() up to times times, stopping as soon as it returns true (simulating retrying a flaky operation), and returns whether it ever succeeded. Test it with an action that uses a captured var counter to fail the first two times and succeed on the third.