Lambdas and Higher-Order Functions
|
This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Kotlin treats functions as values throughout the language — a lambda can be stored in a variable, passed as an argument, or returned from another function, with syntax deliberately optimized for the common case of passing one to a library function.
Lambda Syntax and Function Types
A lambda’s type is written (ParamTypes) → ReturnType. Parameter types are usually inferred from context, so
most lambdas at a call site carry no type annotations at all:
val square: (Int) -> Int = { x -> x * x }
val add = { a: Int, b: Int -> a + b } // types inferred from the explicit parameter annotations
println(square(5)) // 25
println(add(2, 3)) // 5
// a single-parameter lambda can refer to it as "it" instead of naming it
val doubled = listOf(1, 2, 3).map { it * 2 } // [2, 4, 6]
Trailing-Lambda Convention
If a function’s last parameter is a lambda, it can be written outside the parentheses — and if it is the
only argument, the parentheses can be omitted entirely. This is why higher-order library functions like
.map { }, .filter { }, and repeat(n) { } read like built-in control-flow syntax rather than ordinary
function calls:
listOf(1, 2, 3).map({ it * 2 }) // legal but unidiomatic
listOf(1, 2, 3).map { it * 2 } // idiomatic: trailing lambda, no parens needed at all
repeat(3) { i -> println("iteration $i") } // "repeat" takes (times: Int, action: (Int) -> Unit)
Closures
A lambda captures (closes over) variables from its enclosing scope, and — unlike Java’s lambdas, which can only
capture effectively final variables — a Kotlin lambda can mutate a captured var:
fun makeCounter(): () -> Int {
var count = 0
return { count++ } // captures and mutates "count" across calls
}
val counter = makeCounter()
println(counter()) // 0
println(counter()) // 1
println(counter()) // 2
Higher-Order Functions
A function that takes another function as a parameter, or returns one, is a higher-order function — the
foundation .map, .filter, and every other functional collection operation
(Collections and Sequences) are built on:
fun <T, R> transform(value: T, operation: (T) -> R): R = operation(value)
val result = transform(5) { it * it } // 25 -- "operation" is the trailing lambda
inline, noinline, and crossinline
Ordinarily, passing a lambda allocates a function-object instance at run time, and calling it goes through an
indirect call — overhead that matters in a hot loop. Marking a higher-order function inline tells the
compiler to paste the function’s bytecode, and its lambda arguments' bodies, directly at each call site — eliminating both the allocation and the indirection entirely. This is also what lets return inside an inlined
lambda perform a non-local return from the enclosing function (the behavior seen in
Control Flow's forEach example, which is itself inline):
inline fun <T> measureAndRun(label: String, block: () -> T): T {
val start = System.nanoTime()
val result = block()
println("$label took ${(System.nanoTime() - start) / 1_000_000}ms")
return result
}
fun findFirstEven(numbers: List<Int>): Int? {
measureAndRun("search") {
for (n in numbers) {
if (n % 2 == 0) return n // non-local return: exits findFirstEven, not just the lambda --
} // only legal because measureAndRun is "inline"
}
return null
}
Two refinements narrow what an inline function’s lambda parameters may do:
-
noinline— exempts one specific lambda parameter from being inlined (so it can be stored in a variable or passed to a non-inline function), when the rest of the function’s lambdas should still be inlined. -
crossinline— still inlines the lambda’s code, but forbids a non-localreturnfrom inside it, needed when the lambda is itself invoked from another, non-inline context (e.g. inside a nested object expression or another lambda) where an early return would be ambiguous.
See Also
-
Control Flow — labeled
return@labelas the alternative to a non-local return. -
Extension Functions and Scope Functions —
let/run/apply/also, themselvesinlinehigher-order functions. -
Type-Safe Builders and DSLs — lambdas with an explicit receiver type.