In the previous tutorial, you learned about collections. Now let’s learn about lambdas and higher-order functions. These are some of the most powerful features in Kotlin.
A lambda is a function without a name. You already used lambdas with filter, map, and other collection functions. In this tutorial, you will learn how they work in depth.
In this tutorial, you will learn:
- Lambda syntax and the
itkeyword - Trailing lambdas
- Function types like
(Int) -> String - Higher-order functions (functions that take or return functions)
- Function references
- Closures
- Inline functions
Lambda Syntax
A lambda is written between curly braces { }. The arrow -> separates the parameters from the body.
// Full syntax: { parameters -> body }
val add = { a: Int, b: Int -> a + b }
println(add(3, 5)) // 8
A lambda with no parameters has no arrow:
val greet = { println("Hello from a lambda!") }
greet()
The last expression in a lambda is its return value:
val classify = { score: Int ->
val label = if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"
label // This is the return value
}
println(classify(85)) // B
You do not use the return keyword in lambdas. The last expression is always the result.
The it Keyword
When a lambda has exactly one parameter, you can skip the parameter name and use it instead.
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Without it — explicit parameter name
val evens = numbers.filter { number -> number % 2 == 0 }
// With it — shorter and cleaner
val odds = numbers.filter { it % 2 != 0 }
More examples:
val doubled = numbers.map { it * 2 }
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
val names = listOf("alex", "sam", "jordan")
val upper = names.map { it.uppercase() }
// [ALEX, SAM, JORDAN]
Use it when the lambda is short and the meaning is clear. For longer lambdas or when using nested lambdas, give the parameter a name for clarity.
Trailing Lambdas
When the last parameter of a function is a lambda, you can move it outside the parentheses. This is called a trailing lambda.
val numbers = listOf(1, 2, 3, 4, 5)
// Without trailing lambda
val result1 = numbers.filter({ it > 3 })
// With trailing lambda — cleaner
val result2 = numbers.filter { it > 3 }
If the lambda is the only argument, you can drop the parentheses completely:
numbers.forEach { println("Number: $it") }
This is why collection operations look so clean in Kotlin. You can chain trailing lambdas:
val result = numbers
.filter { it > 2 }
.map { it * 10 }
.sorted()
println(result) // [30, 40, 50]
Function Types
In Kotlin, functions have types. You can store a function in a variable, pass it to another function, or return it from a function.
Here are the common function types:
| Type | Meaning |
|---|---|
(Int, Int) -> Int | Takes two Ints, returns an Int |
(String) -> Boolean | Takes a String, returns a Boolean |
() -> String | Takes nothing, returns a String |
(String) -> Unit | Takes a String, returns nothing |
Examples:
val add: (Int, Int) -> Int = { a, b -> a + b }
val subtract: (Int, Int) -> Int = { a, b -> a - b }
println(add(10, 3)) // 13
println(subtract(10, 3)) // 7
// No parameters
val greeting: () -> String = { "Hello, World!" }
println(greeting()) // Hello, World!
// Unit return (no meaningful return value)
val logger: (String) -> Unit = { message -> println("LOG: $message") }
logger("Something happened")
You can even have nullable function types:
val callback: ((String) -> Unit)? = null
callback?.invoke("This won't print") // Safe call on nullable function
Higher-Order Functions
A higher-order function is a function that takes another function as a parameter or returns a function. This is where Kotlin becomes very powerful.
Functions That Take Functions
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
// Pass different lambdas
println(calculate(10, 5) { a, b -> a + b }) // 15
println(calculate(10, 5) { a, b -> a * b }) // 50
println(calculate(10, 5) { a, b -> a - b }) // 5
A custom filter function:
fun <T> filterList(list: List<T>, predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in list) {
if (predicate(item)) {
result.add(item)
}
}
return result
}
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val bigNumbers = filterList(numbers) { it > 5 }
println(bigNumbers) // [6, 7, 8, 9, 10]
Functions That Return Functions
fun createMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
val triple = createMultiplier(3)
val quadruple = createMultiplier(4)
println(triple(7)) // 21
println(quadruple(7)) // 28
This pattern is useful for creating reusable functions with different configurations.
Function References
You can pass an existing function using ::functionName. This is called a function reference.
fun isEven(n: Int): Boolean = n % 2 == 0
fun square(n: Int): Int = n * n
val numbers = listOf(1, 2, 3, 4, 5, 6)
// Pass function references instead of lambdas
val evens = numbers.filter(::isEven)
println(evens) // [2, 4, 6]
val squared = numbers.map(::square)
println(squared) // [1, 4, 9, 16, 25, 36]
You can also reference methods on a class:
val names = listOf("Alex", "Sam", "Jordan")
val lengths = names.map(String::length)
println(lengths) // [4, 3, 6]
Closures
A closure is a lambda that captures variables from its surrounding scope. The lambda can read and modify these variables.
var total = 0
val numbers = listOf(10, 20, 30, 40, 50)
// This lambda captures `total` from the outer scope
numbers.forEach { total += it }
println(total) // 150
You can use closures to create stateful functions:
fun createCounter(): () -> Int {
var count = 0
return {
count++
count
}
}
val counter = createCounter()
println(counter()) // 1
println(counter()) // 2
println(counter()) // 3
Each call to createCounter() creates a new counter with its own count variable. This is because the lambda closes over the variable.
Inline Functions
The inline keyword tells the compiler to copy the function body directly into the call site. This avoids creating a lambda object at runtime.
inline fun measureTime(action: () -> Unit): Long {
val start = System.currentTimeMillis()
action()
val end = System.currentTimeMillis()
return end - start
}
val time = measureTime {
val sum = (1..1_000_000).sum()
println("Sum: $sum")
}
println("Time: ${time}ms")
Without inline, Kotlin creates an anonymous class for each lambda. With inline, the compiler replaces the function call with the actual code. This makes a difference in performance-critical code.
Use inline for:
- Small higher-order functions that are called frequently
- Functions where lambda overhead matters (like in tight loops)
The Kotlin standard library uses inline for functions like let, run, apply, also, filter, map, and more.
Lambda vs Anonymous Function
Kotlin also has anonymous functions. They look like regular functions but without a name:
val double = fun(x: Int): Int {
return x * 2
}
println(double(5)) // 10
The difference is:
| Feature | Lambda | Anonymous Function |
|---|---|---|
| Syntax | { x -> x * 2 } | fun(x: Int): Int { return x * 2 } |
| Return | Last expression | return keyword |
| Return type | Inferred | Can be specified |
In practice, lambdas are used 95% of the time. Anonymous functions are rare but useful when you need an explicit return type.
Common Lambda Patterns
Here are patterns you will see often in Kotlin code.
Sorting with Lambda
val names = listOf("Jordan", "Alex", "Sam")
// Sort by length
val byLength = names.sortedBy { it.length }
println(byLength) // [Sam, Alex, Jordan]
// Sort descending
val byLengthDesc = names.sortedByDescending { it.length }
println(byLengthDesc) // [Jordan, Alex, Sam]
Grouping with Lambda
val words = listOf("apple", "avocado", "banana", "blueberry", "cherry")
val byFirstLetter = words.groupBy { it.first() }
println(byFirstLetter)
// {a=[apple, avocado], b=[banana, blueberry], c=[cherry]}
Building Strings with Lambda
val names = listOf("Alex", "Sam", "Jordan")
val result = buildString {
names.forEachIndexed { index, name ->
append("${index + 1}. $name")
if (index < names.size - 1) append(", ")
}
}
println(result) // 1. Alex, 2. Sam, 3. Jordan
Conditional Execution
val numbers = listOf(1, 2, 3, 4, 5)
val hasEven = numbers.any { it % 2 == 0 }
println("Has even: $hasEven") // true
val allPositive = numbers.all { it > 0 }
println("All positive: $allPositive") // true
val noneNegative = numbers.none { it < 0 }
println("None negative: $noneNegative") // true
Practical Example: Products
Let’s combine everything we learned:
data class Product(val name: String, val price: Double, val category: String)
val products = listOf(
Product("Laptop", 999.99, "Electronics"),
Product("Book", 19.99, "Education"),
Product("Phone", 699.99, "Electronics"),
Product("Pen", 2.99, "Office"),
Product("Tablet", 499.99, "Electronics"),
Product("Notebook", 5.99, "Office")
)
// Find expensive electronics using lambdas
val expensiveElectronics = products
.filter { it.category == "Electronics" }
.filter { it.price > 500 }
.sortedByDescending { it.price }
.map { it.name }
println(expensiveElectronics) // [Laptop, Phone]
// Calculate total by category using higher-order functions
val totalByCategory = products
.groupBy { it.category }
.mapValues { (_, items) -> items.sumOf { it.price } }
println(totalByCategory)
// {Electronics=2199.97, Education=19.99, Office=8.98}
// Apply discount using a lambda variable
val applyDiscount: (Product, Double) -> Product = { product, discount ->
product.copy(price = product.price * (1 - discount))
}
val discounted = products.map { applyDiscount(it, 0.1) }
discounted.forEach { println("${it.name}: $${String.format("%.2f", it.price)}") }
Lambda Performance Tips
Here are some tips for working with lambdas:
Use function references when possible.
list.filter(::isEven)is slightly more efficient thanlist.filter { isEven(it) }because the compiler can optimize it better.Prefer
inlinefor small, frequently called lambdas. The Kotlin standard library marks most collection operations asinline. This meansfilter,map,forEach, and similar functions do not create lambda objects at runtime.Avoid capturing mutable state in lambdas unless you need to. Closures that modify outer variables can make code harder to understand and debug.
Keep lambdas short. If a lambda is more than 5-10 lines, consider extracting it into a named function. This makes your code easier to read and test.
Use trailing lambda syntax. It makes your code look cleaner and more Kotlin-like. Most Kotlin developers expect this style.
Summary
Here is a quick reference for everything you learned:
| Concept | Example |
|---|---|
| Lambda | { a: Int, b: Int -> a + b } |
it keyword | list.filter { it > 5 } |
| Trailing lambda | list.filter { it > 5 } |
| Function type | (Int, Int) -> Int |
| Higher-order function | fun calc(op: (Int) -> Int) |
| Function reference | list.filter(::isEven) |
| Closure | Lambda that captures outer variables |
| Inline function | inline fun run(action: () -> Unit) |
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In this tutorial, you learned about lambdas, higher-order functions, function types, closures, and inline functions. These concepts are everywhere in Kotlin code.
In the next tutorial, you will learn about extension functions and properties — a way to add new functionality to existing classes without modifying them.
This is part 9 of the Kotlin Tutorial series. Check out Part 8: Collections if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.