Kotlin Functional Programming — Higher-Order Functions, Sequences, Scope Functions

Functional Programming in Kotlin

Kotlin is a multi-paradigm language that supports both object-oriented and functional programming. It provides core functional programming elements at the language level, including first-class functions, higher-order functions, lambdas, and immutable collections.

This article covers higher-order functions, collection operations, sequences, and scope functions with practical patterns.

Higher-Order Functions and Lambdas

A higher-order function is a function that takes a function as a parameter or returns one. In Kotlin, these are written concisely using lambda expressions.

// Higher-order function definition — Takes a function as parameter
fun <T> List<T>.customFilter(predicate: (T) -> Boolean): List<T> {
    val result = mutableListOf<T>()
    for (item in this) {
        if (predicate(item)) {
            result.add(item)
        }
    }
    return result
}

// Higher-order function that returns a function
fun createMultiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }  // Closure — captures factor
}

// Inline function — Eliminates lambda overhead
inline fun <T> measureTime(label: String, block: () -> T): T {
    val start = System.nanoTime()
    val result = block()
    val elapsed = (System.nanoTime() - start) / 1_000_000.0
    println("$label: ${String.format("%.2f", elapsed)}ms")
    return result
}

fun main() {
    // Using higher-order functions
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    val evens = numbers.customFilter { it % 2 == 0 }
    println("Evens: $evens")
    // Output: Evens: [2, 4, 6, 8, 10]

    val bigNumbers = numbers.customFilter { it > 5 }
    println("Greater than 5: $bigNumbers")
    // Output: Greater than 5: [6, 7, 8, 9, 10]

    // Returning functions
    val double = createMultiplier(2)
    val triple = createMultiplier(3)
    println("double(5) = ${double(5)}")  // 10
    println("triple(5) = ${triple(5)}")  // 15
    // Output:
    // double(5) = 10
    // triple(5) = 15

    // Function composition
    val doubleAndAdd10: (Int) -> Int = { double(it) + 10 }
    println("doubleAndAdd10(7) = ${doubleAndAdd10(7)}")
    // Output: doubleAndAdd10(7) = 24

    // measureTime inline function
    val sorted = measureTime("Sort") {
        (1..100_000).toList().shuffled().sorted()
    }
    println("First 5 sorted: ${sorted.take(5)}")
    // Output:
    // Sort: 45.23ms (varies by system)
    // First 5 sorted: [1, 2, 3, 4, 5]
}

inline functions inline the lambda at the call site, eliminating object creation overhead. Use them for utility functions that frequently invoke small lambdas.

Functional Collection Operations

Kotlin’s standard library provides rich collection operations such as map, filter, flatMap, groupBy, and fold.

data class Student(
    val name: String,
    val grade: Int,
    val scores: List<Int>
)

fun main() {
    val students = listOf(
        Student("Alice", 1, listOf(90, 85, 92)),
        Student("Bob", 2, listOf(78, 82, 88)),
        Student("Charlie", 1, listOf(95, 91, 87)),
        Student("Diana", 2, listOf(88, 93, 95)),
        Student("Eve", 3, listOf(72, 68, 75))
    )

    // map — Transform
    val names = students.map { it.name }
    println("Names: $names")
    // Output: Names: [Alice, Bob, Charlie, Diana, Eve]

    // filter + map chaining
    val topStudents = students
        .filter { it.scores.average() >= 85 }
        .map { "${it.name} (avg: ${"%.1f".format(it.scores.average())})" }
    println("Top students: $topStudents")
    // Output: Top students: [Alice (avg: 89.0), Charlie (avg: 91.0), Diana (avg: 92.0)]

    // groupBy — Grouping
    val byGrade = students.groupBy { it.grade }
    byGrade.forEach { (grade, group) ->
        println("Grade $grade: ${group.map { it.name }}")
    }
    // Output:
    // Grade 1: [Alice, Charlie]
    // Grade 2: [Bob, Diana]
    // Grade 3: [Eve]

    // flatMap — Flatten nested lists
    val allScores = students.flatMap { it.scores }
    println("All scores: $allScores")
    // Output: All scores: [90, 85, 92, 78, 82, 88, 95, 91, 87, 88, 93, 95, 72, 68, 75]

    // fold — Accumulating operation
    val totalAverage = students.fold(0.0) { acc, student ->
        acc + student.scores.average()
    } / students.size
    println("Overall average: ${"%.1f".format(totalAverage)}")
    // Output: Overall average: 84.6

    // associate — Create a Map
    val scoreMap = students.associate { it.name to it.scores.average() }
    println("Score map: $scoreMap")
    // Output: Score map: {Alice=89.0, Bob=82.67, Charlie=91.0, Diana=92.0, Eve=71.67}

    // partition — Split by condition
    val (passed, failed) = students.partition { it.scores.average() >= 80 }
    println("Passed: ${passed.map { it.name }}")
    println("Failed: ${failed.map { it.name }}")
    // Output:
    // Passed: [Alice, Bob, Charlie, Diana]
    // Failed: [Eve]
}

Sequences — Lazy Evaluation

Collection operations create intermediate lists at each step. Sequence processes the pipeline without intermediate collections through lazy evaluation.

fun main() {
    val numbers = (1..1_000_000).toList()

    // Eager evaluation — Creates 2 intermediate lists
    val eagerResult = numbers
        .filter { it % 2 == 0 }    // Intermediate list 1 (500K elements)
        .map { it * 2 }            // Intermediate list 2 (500K elements)
        .take(5)
    println("Eager: $eagerResult")
    // Output: Eager: [4, 8, 12, 16, 20]

    // Lazy evaluation — No intermediate lists, only processes 5 elements
    val lazyResult = numbers.asSequence()
        .filter { it % 2 == 0 }    // No intermediate list
        .map { it * 2 }            // No intermediate list
        .take(5)                   // Processes only 5 elements then stops
        .toList()
    println("Lazy: $lazyResult")
    // Output: Lazy: [4, 8, 12, 16, 20]

    // Infinite sequence — generateSequence
    val fibonacci = generateSequence(Pair(0L, 1L)) { (a, b) ->
        Pair(b, a + b)
    }.map { it.first }

    val first10 = fibonacci.take(10).toList()
    println("Fibonacci 10: $first10")
    // Output: Fibonacci 10: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    // Sequence builder — Emit values with yield
    val primes = sequence {
        yield(2)
        var n = 3
        while (true) {
            if ((2 until n).none { n % it == 0 }) {
                yield(n)
            }
            n += 2
        }
    }

    println("First 15 primes: ${primes.take(15).toList()}")
    // Output: First 15 primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
}

Sequences are effective when the number of elements is large and there are multiple intermediate operations. For few elements or a single operation, regular collections can be faster.

Scope Functions — let, run, apply, also, with

Scope functions execute a code block within the context of an object. The differences between them lie in how they reference the object (this/it) and what they return (context object/lambda result).

data class Config(
    var host: String = "",
    var port: Int = 0,
    var debug: Boolean = false
)

fun main() {
    // let — Null check + transform (references with it, returns lambda result)
    val name: String? = "Kotlin"
    val length = name?.let {
        println("Name: $it")
        it.length  // Return value
    }
    println("Length: $length")
    // Output:
    // Name: Kotlin
    // Length: 6

    // run — Object initialization + result computation (references with this, returns lambda result)
    val greeting = Config().run {
        host = "localhost"
        port = 8080
        "Server: $host:$port"  // Return value
    }
    println(greeting)
    // Output: Server: localhost:8080

    // apply — Object configuration (references with this, returns the object itself)
    val config = Config().apply {
        host = "api.example.com"
        port = 443
        debug = false
    }
    println("Config: $config")
    // Output: Config: Config(host=api.example.com, port=443, debug=false)

    // also — Side effects (references with it, returns the object itself)
    val numbers = mutableListOf(3, 1, 4, 1, 5)
        .also { println("Before sort: $it") }
        .also { it.sort() }
        .also { println("After sort: $it") }
    // Output:
    // Before sort: [3, 1, 4, 1, 5]
    // After sort: [1, 1, 3, 4, 5]

    // with — Multiple operations on an existing object (references with this, returns lambda result)
    val info = with(config) {
        """
        |Host: $host
        |Port: $port
        |Debug: $debug
        """.trimMargin()
    }
    println(info)
    // Output:
    // Host: api.example.com
    // Port: 443
    // Debug: false
}

Scope Function Selection Guide

FunctionReferenceReturn ValuePrimary Use
letitLambda resultNull check, transformation
runthisLambda resultInitialization + result computation
applythisObject itselfObject configuration
alsoitObject itselfSide effects (logging, validation)
withthisLambda resultMultiple operations on existing object

Practical Tips

  • Immutable collections by default: Use listOf and mapOf as defaults, and only use mutableListOf when mutation is needed.
  • When to use sequences: Consider asSequence() when there are 10,000+ elements and 2+ intermediate operations.
  • Avoid scope function overuse: Nesting 3+ levels of scope functions hurts readability. Extracting to local variables may be clearer.
  • Use inline functions: When higher-order functions are called frequently, use inline to eliminate lambda object creation overhead.
  • Destructuring + collections: Combining map, filter with destructuring declarations makes Pair and Map.Entry handling concise.

Was this article helpful?