Skip to content

Kotlin Lambdas — Higher-Order Functions and Closures Guide

DodaTech Updated 2026-06-28 9 min read

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

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

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

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

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

  5. Confusing function types with FunctionN interfaces: Under the hood, Kotlin functions compile to Function0 through Function22 interfaces. You rarely need to use these directly.

  6. Not using underscore for unused parameters: In lambdas with multiple parameters, use _ for unused ones: { _, b -> b * 2 }.

Practice Questions

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

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

  1. 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).

  1. 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 }.

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

Can a lambda modify a local variable from its enclosing scope?

Yes. Lambdas can read and modify local variables. This is called capturing or closure. The captured variables must be effectively final in Java, but Kotlin allows mutation.

What is the maximum number of parameters a lambda can have?

Kotlin supports up to 22 parameters for function types. In practice, more than 3-4 parameters suggests a design issue.

How do I write a lambda that returns a value?

The last expression in a lambda is the return value. No explicit return statement is needed, unless you use a labeled return for early exit.

What is the difference between { it * 2 } and fun(it: Int): Int = it * 2?

Both are function literals. The first is a lambda (shorter). The second is an anonymous function (allows explicit return type and non-local returns behave differently).

Can I store a lambda in a variable?

Yes. Assign it to a variable with a function type: val myLambda: (Int) -> Int = { it * 2 }. Then call it with myLambda(5).

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