Kotlin Lambdas and Higher-Order Functions

Write functions as values with lambdas, pass them to other functions, use it and trailing lambda syntax, return functions, and reference existing functions with ::.

Lesson 12 of 43Intermediate19 min read

In the last lesson you passed small blocks of code to filter, map and sortedBy. Those blocks are lambdas: functions without a name, written right where they're needed. In Kotlin, functions are values like any other. You can store them in variables, pass them to other functions and return them as results.

A function that takes or returns another function is called a higher-order function. They let you separate what to do from how to repeat or organise it. filter knows how to walk through a list; you just tell it which items to keep.

In this lesson you'll write lambdas with and without names for their parameters, read function types like (Int) -> Boolean, write your own higher-order functions, use the trailing lambda syntax that makes Kotlin code so readable, and reference existing functions with ::.

Lambda syntax and function types

Kotlin
val double: (Int) -> Int = { n -> n * 2 }
val add = { a: Int, b: Int -> a + b }
println(double(5))      // 10
println(add(2, 3))      // 5
  • A lambda is written in curly braces: parameters, then ->, then the body.
  • The last expression in the body is the result; there's no return.
  • A function type describes inputs and output: (Int) -> Int takes an Int and returns one; (String, Int) -> Boolean takes two values; () -> Unit takes nothing and returns nothing.
  • If the type is known, parameter types can be left out: val double: (Int) -> Int = { n -> n * 2 }.

it and trailing lambdas

When a lambda has exactly one parameter, you can skip naming it and use it:

Kotlin
val isEven: (Int) -> Boolean = { it % 2 == 0 }

If a function's last parameter is a function, the lambda can go outside the brackets. If it's the only argument, the brackets can disappear entirely:

Kotlin
numbers.filter({ it > 0 })     // allowed, but unusual
numbers.filter() { it > 0 }    // trailing lambda
numbers.filter { it > 0 }      // the usual style

This is why so much Kotlin code reads like repeat(3) { ... } or list.forEach { ... }.

Writing higher-order functions

Declare a parameter with a function type, then call it like a normal function:

Kotlin
fun applyTwice(x: Int, f: (Int) -> Int): Int = f(f(x))

applyTwice(3) { it + 10 }      // 23

A function can also return a function, which is useful for building customised behaviour:

Kotlin
fun multiplier(factor: Int): (Int) -> Int = { it * factor }
val triple = multiplier(3)
triple(7)                       // 21

The returned lambda remembers factor even after multiplier has finished. That's called a closure.

Function references and returning

If an existing function already does the job, pass a reference to it with :: instead of wrapping it in a lambda:

Kotlin
fun isLong(word: String) = word.length > 5
words.filter(::isLong)
words.map(String::uppercase)

Inside a lambda, a plain return would return from the enclosing function, which is rarely what you want. To skip to the next item in forEach, use a labelled return: return@forEach. Usually the clearer fix is to use filter first, so there's nothing to skip.

Closures that change captured variables

A lambda doesn't only read the variables around it; it can change them too. Each call to makeCounter below creates a fresh count, and the returned lambda keeps hold of it:

Kotlin
fun makeCounter(): () -> Int {
    var count = 0
    return { ++count }
}

val next = makeCounter()
println(next())    // 1
println(next())    // 2
val other = makeCounter()
println(other())   // 1, a separate count

This is how lambdas can carry private state without a class. Use it sparingly, though: when several lambdas change the same variable, it gets hard to follow who changed what.

Unpacking parameters, optional callbacks and lambdas in maps

When a lambda receives a pair or a map entry, you can unpack it in brackets, and replace any part you don't need with _:

Kotlin
prices.forEach { (item, price) -> println("$item: $price") }
prices.forEach { (_, price) -> println(price) }

Function types can be nullable, which is a neat way to make a callback optional. Wrap the type in brackets before the ?, and call it with ?.invoke:

Kotlin
fun download(url: String, onDone: ((String) -> Unit)? = null) {
    onDone?.invoke("done: $url")
}

download("a.txt")                      // no callback, nothing printed
download("b.txt") { println(it) }      // prints done: b.txt

Because lambdas are values, you can also store them in collections. A map from symbols to operations makes a tiny calculator with no when at all:

Kotlin
val ops: Map<String, (Int, Int) -> Int> = mapOf(
    "+" to { a, b -> a + b },
    "*" to { a, b -> a * b },
)
println(ops["*"]?.invoke(6, 7))   // 42

The lookup returns a nullable function, since the key might be missing, so the safe call ?.invoke is needed here as well.

Kotlin Lambdas Syntax

Lambda forms

val f: (Int) -> Int = { x -> x * 2 }
val g = { a: Int, b: Int -> a + b }
val h: (String) -> Int = { it.length }
fun run(action: () -> Unit) = action()
run { println("hi") }
fun maker(n: Int): (Int) -> Int = { it + n }
list.filter(::isValid)
list.forEach { if (it < 0) return@forEach; println(it) }

Kotlin Lambdas Summary

  • A lambda is a function value in braces: { a, b -> a + b }; its last expression is the result.
  • Function types like (Int) -> Boolean describe parameters and return types.
  • it names a single parameter; a trailing lambda goes outside the brackets.
  • Higher-order functions take or return functions; returned lambdas capture variables as closures.
  • ::name references an existing function; use return@label inside lambdas.

Kotlin Lambdas Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Lambdas as values

Kotlin
fun main() {
    val square: (Int) -> Int = { it * it }
    val greet = { name: String -> "Hello, $name!" }
    val isAdult: (Int) -> Boolean = { age -> age >= 18 }

    println(square(6))
    println(greet("Priya"))
    println(isAdult(16))

    val operations = mapOf<String, (Int, Int) -> Int>(
        "add" to { a, b -> a + b },
        "multiply" to { a, b -> a * b }
    )
    println(operations["multiply"]?.invoke(4, 5))
}
Output
36
Hello, Priya!
false
20
Line-by-line explanation
  1. Line 2With a single parameter, it refers to the value.
  2. Line 3The parameter type is written inside the lambda, so the variable's type is inferred.
  3. Lines 10 to 13Functions stored in a map, looked up by name.
  4. Line 14The map returns a nullable function, so it's called safely with ?.invoke.
Example 2

Your own higher-order function

Kotlin
fun retry(times: Int, action: (Int) -> Boolean): Boolean {
    for (attempt in 1..times) {
        if (action(attempt)) {
            println("Succeeded on attempt $attempt")
            return true
        }
        println("Attempt $attempt failed")
    }
    return false
}

fun main() {
    val ok = retry(3) { attempt -> attempt == 2 }
    println("Result: $ok")
}
Output
Attempt 1 failed
Succeeded on attempt 2
Result: true
Line-by-line explanation
  1. Line 1action is a function that receives the attempt number and reports success.
  2. Line 3Calling the parameter runs the caller's lambda.
  3. Line 13A trailing lambda supplies the behaviour; here it pretends the second attempt works.
Example 3

Closures and function references

Kotlin
fun counter(): () -> Int {
    var count = 0
    return { ++count }
}

fun isLong(word: String) = word.length > 5

fun main() {
    val next = counter()
    println(next())
    println(next())
    println(next())

    val words = listOf("kotlin", "map", "lambda", "it")
    println(words.filter(::isLong))
    println(words.map(String::length))
}
Output
1
2
3
[kotlin, lambda]
[6, 3, 6, 2]
Line-by-line explanation
  1. Lines 2 to 3The lambda captures count, so it keeps its value between calls.
  2. Lines 9 to 12Each call to next increases the same captured variable.
  3. Line 15A reference to isLong is passed instead of writing { isLong(it) }.
  4. Line 16String::length refers to a property of each string.

Common Kotlin Lambdas Mistakes

Writing return inside a lambda

Don't
fun printPositives(xs: List<Int>) {
    xs.forEach {
        if (it < 0) return         // leaves printPositives completely
        println(it)
    }
}
Do
fun printPositives(xs: List<Int>) {
    xs.filter { it >= 0 }.forEach { println(it) }
}

Using it with two parameters

Don't
val add: (Int, Int) -> Int = { it + it }   // error: it is only for one parameter
Do
val add: (Int, Int) -> Int = { a, b -> a + b }

Forgetting to call the function

Don't
fun run(action: () -> Unit) {
    action                       // does nothing
}
Do
fun run(action: () -> Unit) {
    action()
}

Kotlin Lambdas in Real Projects

Lambdas in Kotlin code

Lambdas are everywhere in Kotlin: click listeners in Android (button.setOnClickListener { ... }), every Jetpack Compose screen, collection pipelines, coroutine builders like launch { } and configuration DSLs such as Gradle's Kotlin build scripts. Learning to read trailing lambdas fluently is one of the most useful Kotlin skills you can build.

Kotlin Lambdas FAQ

What is the difference between a lambda and a function in Kotlin?

A function declared with fun has a name and lives in a file or class. A lambda is an unnamed piece of code written in braces that you can store in a variable or pass as an argument. You can turn a named function into a value with a reference such as ::isLong, so the two often work interchangeably.

What does it mean in a Kotlin lambda?

it is the implicit name of the single parameter when a lambda has exactly one and you haven't named it. In list.filter { it > 0 }, it is each item in turn. With two or more parameters you must name them, as in { a, b -> a + b }.

Why can't I use return inside a Kotlin lambda?

A plain return inside a lambda passed to an inline function such as forEach returns from the whole enclosing function, not just the lambda, and in other lambdas it isn't allowed at all. Use a labelled return like return@forEach to finish only the lambda, or let the last expression be the result.

What is a higher-order function?

A function that takes another function as a parameter or returns one. filter, map and repeat are all higher-order functions from the standard library. You write your own by giving a parameter a function type, such as f: (Int) -> Int, and calling it with f(x).

How do I pass a function as a parameter in Kotlin?

Declare the parameter with a function type, such as action: () -> Unit or check: (String) -> Boolean. Callers can pass a lambda, usually as a trailing lambda after the brackets, or a reference to an existing function with ::, as in words.filter(::isLong).

Kotlin Lambdas Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the lambda so it returns its number plus one.

val inc: (Int) -> Int = {  + 1 }
Predict the outputeasy

What does this print?

val f = { x: Int -> x * 3 }
println(f(4))
Predict the outputmedium

What does this print?

fun applyTwice(x: Int, f: (Int) -> Int) = f(f(x))
println(applyTwice(2) { it * 5 })
Multiple choicemedium

What does the type (String) -> Boolean describe?

Predict the outputhard

What does this print?

fun adder(n: Int): (Int) -> Int = { it + n }
val add5 = adder(5)
val add10 = adder(10)
println(add5(1) + add10(1))

Kotlin Lambdas Quiz

5 questions to check what you learned.

  1. What is a lambda?

    • A loop
    • A type of list
    • A class
    • A function without a name, written as a value in braces
  2. What is a higher-order function?

    • A very fast function
    • A function inside a class
    • A function with many parameters
    • A function that takes or returns another function
  3. What does list.filter(::isValid) pass to filter?

    • The result of isValid
    • A new copy of isValid
    • A reference to the isValid function
    • Nothing
  4. When can a lambda be written outside the brackets?

    • When it's the last parameter of the function being called
    • Always
    • Only for forEach
    • Never
  5. What is a closure?

    • A lambda that closes a file
    • A lambda that captures and remembers variables from where it was created
    • A function with no return value
    • A finished coroutine