Kotlin - Lambdas & Higher-Order Functions
Kotlin Lambdas & Higher-Order Functions
Functional programming is a core feature of Kotlin. Learn lambda expressions, higher-order functions, function types, and how to write more expressive and concise code.
Lambda Expressions
Key Concept: Lambda expressions are anonymous functions that can be treated as values - passed to functions, returned from functions, and stored in variables.
Basic Lambda Syntax
// Basic lambda expression
val sum = { a: Int, b: Int -> a + b }
println(sum(5, 3)) // 8
// Lambda with single parameter (it)
val square = { x: Int -> x * x }
val square2 = { it: Int -> it * it } // Explicit parameter name
val square3: (Int) -> Int = { it * it } // Type inference with 'it'
println(square(4)) // 16
println(square2(5)) // 25
println(square3(6)) // 36
// Lambda with no parameters
val greeting = { "Hello, Kotlin!" }
println(greeting()) // Hello, Kotlin!
// Multi-line lambda (last expression is returned)
val complex = { x: Int, y: Int ->
val sum = x + y
val product = x * y
println("Sum: $sum, Product: $product")
sum + product // This is the return value
}
println(complex(3, 4)) // Prints: Sum: 7, Product: 12, then returns 19
Function Types
// Function type declarations
val operation: (Int, Int) -> Int = { a, b -> a + b }
val predicate: (String) -> Boolean = { it.isNotEmpty() }
val processor: (List) -> String = { list -> list.joinToString(", ") }
val action: () -> Unit = { println("Action executed") }
// Nullable function types
val optionalFunction: ((Int) -> Int)? = null
val definiteFunction: ((Int) -> Int)? = { it * 2 }
// Function types with receiver (extension function type)
val stringBuilder: StringBuilder.() -> Unit = {
append("Hello")
append(" ")
append("World")
}
val sb = StringBuilder()
sb.stringBuilder() // or sb.apply(stringBuilder)
println(sb.toString()) // Hello World
Using Lambdas with Collections
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// map - transform each element
val squared = numbers.map { it * it }
println("Squared: $squared") // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// filter - select elements that match condition
val evens = numbers.filter { it % 2 == 0 }
println("Evens: $evens") // [2, 4, 6, 8, 10]
// forEach - perform action on each element
numbers.forEach { println("Number: $it") }
// find - get first element matching condition
val firstLarge = numbers.find { it > 5 }
println("First > 5: $firstLarge") // 6
// any/all - check conditions
val hasEven = numbers.any { it % 2 == 0 }
val allPositive = numbers.all { it > 0 }
println("Has even: $hasEven, All positive: $allPositive") // true, true
// reduce/fold - aggregate values
val sum = numbers.reduce { acc, n -> acc + n }
val product = numbers.fold(1) { acc, n -> acc * n }
println("Sum: $sum, Product: $product")
Higher-Order Functions
Functions that Take Functions as Parameters
// Higher-order function definition
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
// Usage with lambda expressions
val addition = calculate(5, 3) { a, b -> a + b }
val multiplication = calculate(5, 3) { a, b -> a * b }
val subtraction = calculate(5, 3) { a, b -> a - b }
println("5 + 3 = $addition") // 8
println("5 * 3 = $multiplication") // 15
println("5 - 3 = $subtraction") // 2
// More complex higher-order function
fun processStrings(
strings: List,
filter: (String) -> Boolean,
transform: (String) -> String
): List {
return strings.filter(filter).map(transform)
}
val words = listOf("hello", "world", "kotlin", "programming", "is", "fun")
val result = processStrings(
strings = words,
filter = { it.length > 3 }, // Keep words longer than 3 chars
transform = { it.uppercase() } // Convert to uppercase
)
println("Processed: $result") // [HELLO, WORLD, KOTLIN, PROGRAMMING]
Functions that Return Functions
// Function returning a function
fun createMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
val double = createMultiplier(2)
val triple = createMultiplier(3)
println("Double 7: ${double(7)}") // 14
println("Triple 7: ${triple(7)}") // 21
// More complex example: create validators
fun createValidator(minLength: Int, maxLength: Int): (String) -> Boolean {
return { input ->
input.length in minLength..maxLength && input.isNotBlank()
}
}
val passwordValidator = createValidator(8, 20)
val usernameValidator = createValidator(3, 15)
println("Password 'secret123' valid: ${passwordValidator("secret123")}") // true
println("Username 'ab' valid: ${usernameValidator("ab")}") // false
// Function factory with configuration
fun createFormatter(prefix: String = "", suffix: String = ""): (String) -> String {
return { text -> "$prefix$text$suffix" }
}
val htmlBold = createFormatter("", "")
val parentheses = createFormatter("(", ")")
val quotes = createFormatter("\"", "\"")
println(htmlBold("Important")) // Important
println(parentheses("Note")) // (Note)
println(quotes("Hello World")) // "Hello World"
Inline Functions
Performance with inline
// Inline function to avoid lambda overhead
inline fun measureTime(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
// Usage
val time = measureTime {
// Some operation
(1..1000000).sum()
}
println("Operation took ${time / 1_000_000} ms")
// Inline with multiple lambdas
inline fun repeatOperation(times: Int, setup: () -> Unit, operation: () -> Unit) {
setup()
repeat(times) {
operation()
}
}
repeatOperation(
times = 3,
setup = { println("Setting up...") },
operation = { println("Executing operation") }
)
// noinline for specific parameters
inline fun processData(
data: List,
inline transform: (String) -> String,
noinline logger: (String) -> Unit // This lambda won't be inlined
): List {
return data.map { item ->
val transformed = transform(item)
logger("Transformed: $item -> $transformed")
transformed
}
}
val data = listOf("hello", "world")
val result = processData(
data = data,
transform = { it.uppercase() },
logger = { println(it) }
)
crossinline and noinline
// crossinline prevents non-local returns
inline fun runTwice(crossinline action: () -> Unit) {
action()
action()
}
fun testCrossinline() {
runTwice {
println("This runs twice")
// return // This would be a compilation error due to crossinline
}
println("This line executes")
}
// Combining inline, noinline, and crossinline
inline fun complexFunction(
inline fastOperation: () -> String,
noinline slowOperation: () -> String,
crossinline safeOperation: () -> Unit
): String {
safeOperation()
return fastOperation() + slowOperation()
}
Function Composition
Composing Functions
// Function composition helpers
infix fun ((A) -> B).then(f: (B) -> C): (A) -> C {
return { a -> f(this(a)) }
}
infix fun ((B) -> C).compose(f: (A) -> B): (A) -> C {
return { a -> this(f(a)) }
}
// Example functions
val addOne: (Int) -> Int = { it + 1 }
val multiplyByTwo: (Int) -> Int = { it * 2 }
val toString: (Int) -> String = { it.toString() }
// Composition
val addThenMultiply = addOne then multiplyByTwo
val multiplyThenAdd = multiplyByTwo compose addOne
val fullTransform = addOne then multiplyByTwo then toString
println("5 -> add 1 -> multiply 2: ${addThenMultiply(5)}") // 12
println("5 -> multiply 2 -> add 1: ${multiplyThenAdd(5)}") // 11
println("5 -> add 1 -> multiply 2 -> string: ${fullTransform(5)}") // "12"
// Practical composition example
val validateEmail: (String) -> Boolean = { it.contains("@") && it.contains(".") }
val normalizeEmail: (String) -> String = { it.trim().lowercase() }
val extractDomain: (String) -> String = { it.substringAfter("@") }
val emailProcessor = normalizeEmail then { email ->
if (validateEmail(email)) extractDomain(email) else "invalid"
}
println(emailProcessor(" [email protected] ")) // example.com
println(emailProcessor("invalid-email")) // invalid
Practical Examples
Event Handling System
// Event handling with lambdas
class EventManager {
private val listeners = mutableMapOf Unit>>()
fun on(event: String, handler: () -> Unit) {
listeners.getOrPut(event) { mutableListOf() }.add(handler)
}
fun emit(event: String) {
listeners[event]?.forEach { it() }
}
// Higher-order function for one-time events
fun once(event: String, handler: () -> Unit) {
var executed = false
on(event) {
if (!executed) {
executed = true
handler()
}
}
}
}
val eventManager = EventManager()
// Register event handlers
eventManager.on("user_login") {
println("User logged in - updating UI")
}
eventManager.on("user_login") {
println("User logged in - logging activity")
}
eventManager.once("app_start") {
println("App started for the first time")
}
// Emit events
eventManager.emit("app_start") // Prints once
eventManager.emit("app_start") // Doesn't print again
eventManager.emit("user_login") // Prints both handlers
Validation System
// Validation with function composition
typealias Validator = (T) -> Boolean
typealias ValidationRule = Pair, String>
class ValidationResult(val isValid: Boolean, val errors: List)
fun validate(value: T, rules: List>): ValidationResult {
val errors = rules.mapNotNull { (validator, message) ->
if (!validator(value)) message else null
}
return ValidationResult(errors.isEmpty(), errors)
}
// Create validation rules
val required: ValidationRule = ({ it.isNotBlank() } to "Field is required")
val minLength: (Int) -> ValidationRule = { min ->
({ it.length >= min } to "Minimum length is $min characters")
}
val maxLength: (Int) -> ValidationRule = { max ->
({ it.length <= max } to "Maximum length is $max characters")
}
val emailFormat: ValidationRule = (
{ it.contains("@") && it.contains(".") } to "Invalid email format"
)
// Usage
fun validateUser(name: String, email: String, password: String) {
val nameValidation = validate(name, listOf(
required,
minLength(2),
maxLength(50)
))
val emailValidation = validate(email, listOf(
required,
emailFormat
))
val passwordValidation = validate(password, listOf(
required,
minLength(8),
{ it.any { char -> char.isUpperCase() } } to "Must contain uppercase letter",
{ it.any { char -> char.isDigit() } } to "Must contain a number"
))
listOf(
"Name" to nameValidation,
"Email" to emailValidation,
"Password" to passwordValidation
).forEach { (field, result) ->
if (!result.isValid) {
println("$field errors: ${result.errors.joinToString(", ")}")
}
}
}
validateUser("Jo", "invalid-email", "weak")
Data Processing Pipeline
// Functional data processing pipeline
data class Person(val name: String, val age: Int, val city: String, val salary: Double)
class DataProcessor {
private val operations = mutableListOf<(List) -> List>()
fun filter(predicate: (T) -> Boolean): DataProcessor {
operations.add { list -> list.filter(predicate) }
return this
}
fun map(transform: (T) -> R): DataProcessor {
val newProcessor = DataProcessor()
newProcessor.operations.addAll(operations.map { op ->
{ list: List ->
@Suppress("UNCHECKED_CAST")
(op(list as List) as List).map(transform)
}
})
newProcessor.operations.add { list -> list.map(transform) as List }
return newProcessor
}
fun sortedBy(selector: (T) -> Comparable<*>): DataProcessor {
operations.add { list -> list.sortedBy(selector) }
return this
}
fun take(n: Int): DataProcessor {
operations.add { list -> list.take(n) }
return this
}
fun process(input: List): List {
return operations.fold(input) { data, operation -> operation(data) }
}
}
// Usage
val people = listOf(
Person("Alice", 30, "New York", 75000.0),
Person("Bob", 25, "San Francisco", 85000.0),
Person("Charlie", 35, "New York", 95000.0),
Person("Diana", 28, "Chicago", 70000.0),
Person("Eve", 32, "San Francisco", 110000.0)
)
val result = DataProcessor()
.filter { it.age >= 28 } // Adults 28+
.filter { it.salary >= 75000 } // High earners
.sortedBy { it.salary } // Sort by salary
.take(3) // Top 3
.process(people)
println("High earners 28+:")
result.forEach { person ->
println("${person.name}, ${person.age}, $${person.salary}")
}
// Functional approach (alternative)
val functionalResult = people
.filter { it.age >= 28 && it.salary >= 75000 }
.sortedBy { it.salary }
.take(3)
println("\nFunctional approach:")
functionalResult.forEach { person ->
println("${person.name}, ${person.age}, $${person.salary}")
}
Best Practices
✅ Good Practices
- Use lambdas for short, simple operations
- Prefer functional collection operations over manual loops
- Use inline functions for performance-critical higher-order functions
- Keep lambda expressions simple and focused
- Use meaningful parameter names instead of 'it' when multiple parameters exist
- Consider function composition for complex transformations
❌ Avoid
- Complex multi-line lambdas (extract to named functions)
- Overusing higher-order functions where simple functions would be clearer
- Creating deeply nested lambda expressions
- Using lambdas for operations with significant side effects
- Ignoring performance implications of lambda allocation in hot paths
Architecture Note: Lambdas and higher-order functions enable powerful functional programming patterns in Kotlin. They're essential for collection processing, event handling, and creating flexible, reusable APIs. Use inline functions for performance-critical code to eliminate lambda overhead.
Practice Exercises
- Create a custom collection processing library using higher-order functions
- Build an event system that supports different types of handlers
- Implement a validation framework using function composition
- Create a simple functional reactive programming system
- Build a query builder using lambda expressions and method chaining
Quick Quiz
- What's the syntax for a lambda with two parameters?
- When should you use inline functions?
- What's the difference between map and forEach?
- How do you create a function that returns another function?
Show answers
- `{ a, b -> /* body */ }` or `{ a: Type, b: Type -> /* body */ }`
- For performance-critical higher-order functions to eliminate lambda call overhead
- `map` transforms and returns a new collection, `forEach` performs side effects and returns Unit
- Define a function with return type `(ParameterType) -> ReturnType` and return a lambda