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
| Function | Reference | Return Value | Primary Use |
|---|---|---|---|
let | it | Lambda result | Null check, transformation |
run | this | Lambda result | Initialization + result computation |
apply | this | Object itself | Object configuration |
also | it | Object itself | Side effects (logging, validation) |
with | this | Lambda result | Multiple operations on existing object |
Practical Tips
- Immutable collections by default: Use
listOfandmapOfas defaults, and only usemutableListOfwhen 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
inlineto eliminate lambda object creation overhead. - Destructuring + collections: Combining
map,filterwith destructuring declarations makesPairandMap.Entryhandling concise.