Kotlin Lambdas — Higher-Order Functions and Closures Guide
In this tutorial, you will learn about Kotlin Lambdas. We cover key concepts, practical examples, and best practices to help you master this topic.
Kotlin lambdas are function literals enclosed in curly braces that can be passed as arguments, stored in variables, and used with higher-order functions to create expressive and concise Functional Programming patterns.
What You'll Learn
- Write lambda expressions with explicit and implicit parameters
- Use the trailing lambda syntax for cleaner code
- Capture variables from the enclosing scope (closures)
- Write anonymous functions with explicit return types
- Create higher-order functions that accept lambdas
- Use function types as parameter and return types
- Apply lambdas with collection operations
Why It Matters
Lambdas are central to idiomatic Kotlin. They replace anonymous inner classes from Java and enable functional programming patterns. Collection operations like filter, map, and forEach all take lambdas. Android's Jetpack Compose uses lambdas extensively for UI callbacks. Understanding lambdas is essential for writing concise, expressive Kotlin code that follows community conventions.
Real-World Use
DodaTech uses lambdas for event handling in its Android configuration tool. Button clicks, text changes, and API callbacks all use lambda syntax. On the server side, route handlers in Ktor are lambdas. The internal logging system uses higher-order functions that wrap lambdas with timing and error handling logic.
Learning Path
flowchart LR A[Collections] --> B[Lambdas\nYou are here] B --> C[Classes] style B fill:#f90,color:#fff
Lambda Syntax
A lambda expression is enclosed in curly braces. Parameters are listed before the body, separated by ->
fun main() {
// Lambda variable
val square: (Int) -> Int = { x: Int -> x * x }
println(square(5)) // Output: 25
// Lambda with type inference
val add = { a: Int, b: Int -> a + b }
println(add(3, 4)) // Output: 7
// Lambda with no parameters
val greet = { println("Hello!") }
greet() // Output: Hello!
// Lambda as function argument
fun repeatAction(times: Int, action: () -> Unit) {
for (i in 1..times) action()
}
repeatAction(3) { println("Kotlin") }
// Output: Kotlin (printed 3 times)
}
Output: Each lambda is called like a regular function and produces the expected output.
The it Keyword
When a lambda has exactly one parameter, you can omit the parameter declaration and use it.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// Explicit parameter
val doubled1 = numbers.map { n -> n * 2 }
println(doubled1) // Output: [2, 4, 6, 8, 10]
// Using it (implicit parameter)
val doubled2 = numbers.map { it * 2 }
println(doubled2) // Output: [2, 4, 6, 8, 10]
// Filter with it
val even = numbers.filter { it % 2 == 0 }
println(even) // Output: [2, 4]
// ForEach with it
numbers.forEach { print("$it ") }
println() // Output: 1 2 3 4 5
// SortedBy with it
val sorted = numbers.sortedBy { -it } // descending
println(sorted) // Output: [5, 4, 3, 2, 1]
}
Output: The it keyword makes single-parameter lambdas concise and readable.
Trailing Lambda Syntax
When the last parameter of a function is a lambda, you can move it outside the parentheses.
fun performOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
fun main() {
// Regular call
val result1 = performOperation(10, 5, { a, b -> a + b })
println(result1) // Output: 15
// Trailing lambda (more idiomatic)
val result2 = performOperation(10, 5) { a, b -> a * b }
println(result2) // Output: 50
// Trailing lambda with it
val numbers = listOf(3, 1, 4, 1, 5, 9, 2)
val filtered = numbers.filter { it > 3 }
println(filtered) // Output: [4, 5, 9]
// Combining multiple collection operations
val result = numbers
.filter { it % 2 != 0 }
.map { it * it }
.sortedDescending()
println(result) // Output: [81, 25, 9, 9, 1]
}
Output: Trailing lambda syntax moves the lambda outside the parentheses, making chained operations read naturally.
Closures (Capturing Variables)
Lambdas can access and modify variables from their enclosing scope.
fun makeCounter(): () -> Int {
var count = 0
return {
count++
count
}
}
fun main() {
val counter1 = makeCounter()
println(counter1()) // Output: 1
println(counter1()) // Output: 2
println(counter1()) // Output: 3
val counter2 = makeCounter()
println(counter2()) // Output: 1 (independent state)
// Capturing mutable variables
var sum = 0
val numbers = listOf(1, 2, 3, 4, 5)
numbers.forEach { sum += it }
println(sum) // Output: 15 (sum captured and modified by lambda)
}
Output: Each counter maintains its own state. The forEach lambda captures and modifies the sum variable.
Function Types
Function types specify the parameter and return types of a lambda or function.
// Function type: (Int, Int) -> Int
val add: (Int, Int) -> Int = { a, b -> a + b }
// Function type: (String) -> Unit (Unit means no return value)
val printMessage: (String) -> Unit = { println(it) }
// Nullable function type
val maybeOp: ((Int, Int) -> Int)? = null
// Function type as parameter
fun processNumbers(
a: Int,
b: Int,
onSuccess: (Int) -> Unit,
onError: (String) -> Unit = { println("Error: $it") }
) {
if (b != 0) {
onSuccess(a / b)
} else {
onError("Division by zero")
}
}
fun main() {
processNumbers(10, 2,
onSuccess = { println("Result: $it") }
)
// Output: Result: 5
processNumbers(10, 0,
onSuccess = { println("Result: $it") }
)
// Output: Error: Division by zero
}
Output: The function type system enables flexible callback patterns with default implementations.
Anonymous Functions
Anonymous functions are an alternative to lambdas that allow explicit return type specification.
fun main() {
// Lambda
val square1: (Int) -> Int = { it * it }
// Anonymous function (equivalent)
val square2: (Int) -> Int = fun(x: Int): Int {
return x * x
}
// Anonymous function with expression body
val square3: (Int) -> Int = fun(x: Int) = x * x
println(square1(4)) // Output: 16
println(square2(4)) // Output: 16
println(square3(4)) // Output: 16
// Anonymous function with collection
val numbers = listOf(1, 2, 3)
val doubled = numbers.map(fun(n: Int): Int = n * 2)
println(doubled) // Output: [2, 4, 6]
}
Output: Anonymous functions behave like lambdas but use the fun keyword and can specify return types.
Lambdas with Receivers
Lambda with receiver allows calling methods of a receiver object without explicit qualifiers.
data class StringBuilderResult(val text: String, val length: Int)
fun buildString(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block()
return sb.toString()
}
fun main() {
// lambda with receiver
val result = buildString {
append("Hello, ")
append("Kotlin!")
val len = this.length
println("Length inside: $len")
}
println(result) // Output: Hello, Kotlin!
// (Also prints: Length inside: 14)
// Another example with custom receiver
val html = buildString {
append("<html>")
append("<body>")
append("<h1>Title</h1>")
append("</body>")
append("</html>")
}
println(html)
// Output: <html><body><h1>Title</h1></body></html>
}
Output: The receiver object's methods are accessible directly inside the lambda. This pattern is used by Kotlin's type-safe builders.
Common Mistakes
Using return inside a lambda incorrectly: In a lambda, return returns from the enclosing function (non-local return). Use labeled returns or anonymous functions for early returns inside lambdas.
Ignoring the trailing lambda convention: When a function takes a lambda as the last parameter, move it outside the parentheses. This is the idiomatic Kotlin style and is expected by the community.
Overusing it when explicit names improve readability: For complex lambdas with nested it references, use explicit parameter names. Code like
it.filter { it > it.other }is confusing.Capturing large objects in closures: Lambdas that outlive their creating scope (e.g., callbacks) may hold references to captured variables, causing memory leaks. Be mindful of what you capture.
Confusing function types with FunctionN interfaces: Under the hood, Kotlin functions compile to Function0 through Function22 interfaces. You rarely need to use these directly.
Not using underscore for unused parameters: In lambdas with multiple parameters, use _ for unused ones:
{ _, b -> b * 2 }.
Practice Questions
- What is the difference between a lambda and an anonymous function?
Answer: Lambdas use curly braces and cannot specify the return type explicitly. Anonymous functions use the fun keyword and can specify the return type. Both are function literals.
- What does the it keyword refer to in a lambda?
Answer: it refers to the single implicit parameter of a lambda. It is available when the lambda has exactly one parameter and you do not declare the parameter explicitly.
- What is a non-local return?
Answer: A return statement inside a lambda returns from the enclosing function, not just the lambda. To return only from the lambda, use a labeled return (return@label).
- How do you create a function that returns a lambda?
Answer: Define the return type as a function type. For example: fun multiplier(factor: Int): (Int) -> Int = { x -> x * factor }.
- Challenge: Write a higher-order function that takes a list of integers and a transformation lambda, applies the transformation, and returns the sum of the results. Then use it to compute the sum of squares and sum of cubes.
Answer:
fun transformAndSum(numbers: List<Int>, transform: (Int) -> Int): Int {
return numbers.map(transform).sum()
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val sumOfSquares = transformAndSum(numbers) { it * it }
println(sumOfSquares) // Output: 55
val sumOfCubes = transformAndSum(numbers) { it * it * it }
println(sumOfCubes) // Output: 225
}
Mini Project
Create an event bus system using lambdas and higher-order functions. Requirements:
- Define an EventBus class that stores lambdas by event type
- Implement subscribe(eventType, lambda) to register listeners
- Implement unsubscribe(eventType, lambda) to remove listeners
- Implement emit(eventType, data) to invoke all registered lambdas
- Use a Map<String, MutableList<(Any) -> Unit>> as storage
- Write a test that subscribes multiple listeners and emits events
This project applies lambdas, higher-order functions, and collection operations in a real-world pattern.
FAQ
What's Next
Now that you understand lambdas, learn classes for object-oriented programming. You can also explore data classes for concise model definitions.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro