Higher-Order Functions and Lambda Expressions in Kotlin
Higher-order functions accept other functions as parameters or return them. A classic example is a custom aggregation function on a collection, which takes an initial value and a combining operation to process each element sequentially.
kotlin fun <T, U> Iterable<T>.aggregate( seed: U, operation: (U, T) -> U ): U { var state = seed for (item in this) { state = operation(state, item) } return state }
The operation parameter has a function type (U, T) -> U. Calling this higher-order function requires passing a function instance, where lambda expressions are typically utilized.
kotlin val numbers = listOf(10, 20, 30)
val total = numbers.aggregate(0) { acc, num -> acc + num }
val concatenated = numbers.aggregate("Items:") { acc, num -> "$acc $num" }
val product = numbers.aggregate(1, Int::times)
Function Types
Kotlin defines function types using a notation like (Int, String) -> Boolean. The syntax consists of parameter types in parentheses and a return type. () -> Unit represents a function with no parameters and no meaningful return value.
A function type can include a receiver type, written as A.(B) -> C. This indicates the function is called on a receiver object of type A with an argument of type B, returning type C. Suspended functions use the suspend modifier, such as suspend () -> Unit.
Parameter names can be included for clarity: (x: Int, y: Int) -> Coordinate. Nullable function types require parentheses: ((Int, Int) -> Int)?. Parentheses also control associativity: (Int) -> ((Int) -> Unit) differs from ((Int) -> Int) -> Unit.
Type aliases simplify complex function signatures:
kotlin typealias ClickListener = (View, ClickEvent) -> Unit
Instantiating Function Types
Function type instances can be obtained in several ways:
- Function literals:
- Lambda expressions:
{ a, b -> a * b } - Anonymous functions:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
- Lambda expressions:
- Callable references:
- Functions:
::isOdd,String::toInt - Properties:
List<Int>::size - Constructors:
::Regex - Bound references:
item::toString
- Functions:
- Custom classes implementing the function interface:
kotlin class StringParser : (String) -> Int { override fun invoke(input: String): Int = input.toIntOrNull() ?: -1 } val parser: (String) -> Int = StringParser()
The compiler infers function types when possible: val doubler = { i: Int -> i * 2 } infers (Int) -> Int.
Non-literal values of function types with and without receivers are interchangeable. A value of (A, B) -> C can be assigned where A.(B) -> C is expected, and vice versa.
kotlin val repeatOp: String.(Int) -> String = { times -> this.repeat(times) } val standardOp: (String, Int) -> String = repeatOp
fun execute(f: (String, Int) -> String): String = f("hello", 3)
val output = execute(repeatOp)
Invoking Function Type Instances
Function type instances are invoked via the invoke operator or directly by name: f.invoke(x) or f(x). If the instance has a receiver type, the receiver object becomes the first argument, or it can be called as an extension:
kotlin val concat: (String, String) -> String = String::plus val subtract: Int.(Int) -> Int = Int::minus
println(concat.invoke("A", "B")) println(subtract(10, 5)) println(10.subtract(3))
Lambda Expressions and Anonymous Functions
Lambda expressions and anonymous functions are function literals—undeclared functions passed immediately as expressions.
Lambda Syntax
Full syntax encloses parameters and an arrow before the body: { x: Int, y: Int -> x + y }. If the return type is not Unit, the last expression is returned implicitly.
Trailing Lambdas
If a function's last parameter is a function, the lambda can be placed outside the parentheses. If it's the only parameter, parentheses can be omitted entirely.
kotlin val product = numbers.aggregate(1) { acc, num -> acc * num } run { println("Executing") }
Implicit Single Parameter: it
When a lambda has exactly one parameter and its type can be inferred, the parameter declaration and -> can be omitted. The parameter is accessible via it.
kotlin nummbers.filter { it > 0 }
Returning Values
Lambdas return the value of their last expression. Explicit returns use a qualified label:
kotlin numbers.filter { val isValid = it > 0 return@filter isValid }
Underscore for Unused Variables
Unused lambda parameters can be replaced with an underscore:
kotlin map.forEach { (_, value) -> println(value) }
Anonymous Functions
Anonymous functions allow explicit return type declarations where inference is insufficient. They resemble standard functions but lack a name.
kotlin fun(x: Int, y: Int): Int = x + y
Unlike lambdas, anonymous functions do not support trailing syntax and must be placed inside parentheses. Furthermore, a return statement inside an anonymous function returns from the anonymous function itself, whereas a return inside a lambda returns from the enclosing function.
Closures
Lambdas, anonymous functions, and local functions can access and modify variables declared in their outer scope (closures).
Function Literals with Receivers
Function literals can specify a receiver type, similar to extension functions. Inside the literal body, the receiver object is available via this, and its members can be accessed directly.
kotlin val sum: Int.(Int) -> Int = { other -> this.plus(other) }
Anonymous functions explicitly declare the receiver type before the parameters:
kotlin val sum = fun Int.(other: Int): Int = this + other
When the receiver type is inferred from context, lambdas can serve as function literals with receivers. This is fundamental for building type-safe DSLs:
kotlin class Document { fun header() { /* ... */ } }
fun buildDocument(setup: Document.() -> Unit): Document { val doc = Document() doc.setup() return doc }
buildDocument { header() }