A Kotlin Lambda Is Just a Value


A lambda is a function written as a value — something you can store in a variable, pass to another function, and call later. Once that one idea clicks, a large chunk of Kotlin’s standard library stops looking like special syntax and starts looking like ordinary function calls.

This is part one of two. We start from the first pair of braces and build up to the higher-order functions you reach for every day. Part two gets into references, receivers, the surprising rules for return, and why all of this is essentially free at runtime.

The smallest lambda

val square = { x: Int -> x * x }

Braces, a parameter list, an arrow, a body. You call it like any other function:

square(5)   // 25

square isn’t special — it’s a value, and its type happens to be a function.

Function types

The type of square is (Int) -> Int: takes an Int, returns an Int. You can write that out explicitly:

val square: (Int) -> Int = { x -> x * x }

Notice that once the type is on the left, you can drop it on the right — Kotlin infers that x is an Int. Inference flows both directions.

A function type is always (parameters) -> ReturnType:

() -> Unit          // takes nothing, returns nothing
(String) -> Int     // takes a String, returns an Int
(Int, Int) -> Int   // takes two Ints, returns one

it: the implicit single parameter

When a lambda has exactly one parameter, you can skip declaring it and refer to it as it:

val square: (Int) -> Int = { it * it }

it is everywhere in real Kotlin. Use it when the lambda is short; name the parameter when the body grows or when lambdas nest and two its would collide.

The last expression is the result

A lambda body can have many statements. There’s no return keyword for the value — the last expression is what the lambda evaluates to:

val describe = { n: Int ->
    val parity = if (n % 2 == 0) "even" else "odd"
    "$n is $parity"   // this line is the result
}

describe(4)   // "4 is even"

Writing return inside a lambda means something else entirely — we’ll get to that in part two.

Higher-order functions

A higher-order function is one that takes or returns a function. Passing a lambda is no different from passing any other value:

fun applyTo(x: Int, f: (Int) -> Int): Int = f(x)

applyTo(5, square)        // 25
applyTo(5, { it + 1 })    // 6

Writing your own is just declaring a parameter whose type is a function — f: (Int) -> Int above. That’s the whole trick.

The trailing-lambda convention

When the last parameter of a function is itself a function, Kotlin lets you move the lambda outside the parentheses:

applyTo(5) { it + 1 }

And when the lambda is the only argument, you drop the parentheses entirely. This is why so much Kotlin reads like built-in syntax when it isn’t:

repeat(3) { println("hi") }

repeat is an ordinary library function that takes a lambda — not a language keyword.

Where you’ll actually use them

The everyday payoff is the collection API. Each of these takes a lambda:

val nums = listOf(1, 2, 3, 4, 5)

nums.filter { it % 2 == 0 }   // [2, 4]
nums.map { it * it }          // [1, 4, 9, 16, 25]
nums.forEach { println(it) }  // prints each on its own line

Read top to bottom, each line says exactly what it does. The pre-streams Java equivalent was a loop plus a mutable accumulator for every one of these.

Closures: capturing the surrounding scope

A lambda can use variables from where it was defined — it “closes over” them:

fun counter(): () -> Int {
    var count = 0
    return { count++ }
}

val next = counter()
next()   // 0
next()   // 1

The returned lambda keeps count alive after counter has returned. And here’s a difference from Java: where Java requires captured variables to be effectively final, a Kotlin lambda can read and modify what it captures — exactly what count++ is doing.

Final thoughts

Strip away the syntax and a lambda is a single idea: a function you can treat as a value. Store it, pass it, return it, call it later. Everything else here — it, trailing lambdas, the whole collection API — is convenience stacked on top of that one fact.

Part two picks up from here: function references that let you skip the lambda when a function already exists, lambdas with a receiver (the trick behind apply and every Kotlin DSL), the rules for return inside a lambda, and why passing all these functions around usually costs nothing at all.