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 it keyword
  • 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:

TypeMeaning
(Int, Int) -> IntTakes two Ints, returns an Int
(String) -> BooleanTakes a String, returns a Boolean
() -> StringTakes nothing, returns a String
(String) -> UnitTakes 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:

FeatureLambdaAnonymous Function
Syntax{ x -> x * 2 }fun(x: Int): Int { return x * 2 }
ReturnLast expressionreturn keyword
Return typeInferredCan 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:

  1. Use function references when possible. list.filter(::isEven) is slightly more efficient than list.filter { isEven(it) } because the compiler can optimize it better.

  2. Prefer inline for small, frequently called lambdas. The Kotlin standard library marks most collection operations as inline. This means filter, map, forEach, and similar functions do not create lambda objects at runtime.

  3. Avoid capturing mutable state in lambdas unless you need to. Closures that modify outer variables can make code harder to understand and debug.

  4. 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.

  5. 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:

ConceptExample
Lambda{ a: Int, b: Int -> a + b }
it keywordlist.filter { it > 5 }
Trailing lambdalist.filter { it > 5 }
Function type(Int, Int) -> Int
Higher-order functionfun calc(op: (Int) -> Int)
Function referencelist.filter(::isEven)
ClosureLambda that captures outer variables
Inline functioninline fun run(action: () -> Unit)

Source Code

You can find the complete source code for this tutorial on GitHub:

KT-9 Source Code 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.