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

  1. Create a custom collection processing library using higher-order functions
  2. Build an event system that supports different types of handlers
  3. Implement a validation framework using function composition
  4. Create a simple functional reactive programming system
  5. Build a query builder using lambda expressions and method chaining

Quick Quiz

  1. What's the syntax for a lambda with two parameters?
  2. When should you use inline functions?
  3. What's the difference between map and forEach?
  4. How do you create a function that returns another function?
Show answers
  1. `{ a, b -> /* body */ }` or `{ a: Type, b: Type -> /* body */ }`
  2. For performance-critical higher-order functions to eliminate lambda call overhead
  3. `map` transforms and returns a new collection, `forEach` performs side effects and returns Unit
  4. Define a function with return type `(ParameterType) -> ReturnType` and return a lambda