Kotlin Functions and Lambda Expressions
Functions are first-class citizens in Kotlin — higher-order functions + lambdas + inline let Charlie build OrderProcessor's data processing pipelines in a functional style, reducing code by 60%.
1. What You'll Learn
- Function declarations: default parameters, named arguments, expression bodies
- Higher-order functions: functions as parameters and return values
- Lambda syntax:
{ it -> ... }anditshorthand - Trailing lambda convention:
orders.filter { ... } - Inline functions
inline: eliminate lambda runtime overhead
2. A Developer's Real Story
(1) Pain Point: Duplicate Order Processing Logic
In Charlie's team, every developer wrote their own order filtering/transformation/aggregation logic — 100+ lines of duplicate code scattered across 5 services. Changing one filter condition meant changing 5 places.
(2) Higher-Order Functions Eliminate Duplication
// Generic order processor - one function replaces 5 copies
fun <T> processOrders(
orders: List<Order>,
filter: (Order) -> Boolean,
transform: (Order) -> T
): List<T> = orders.filter(filter).map(transform)
// Usage: each call site is 2 lines instead of 20
val highValueIds = processOrders(orders, { it.total > 1_000 }, { it.id })
val vipCustomers = processOrders(orders, { it.total > 10_000 }, { it.customer })
Higher-order functions parameterize "what changes" so "what stays the same" is written only once.
3. Function Declarations
(1) Basic Functions
// Block body
fun calculateTax(total: Double, rate: Double = 0.08): Double {
return total * rate
}
// Expression body
fun calculateTax(total: Double, rate: Double = 0.08) = total * rate
// Unit return (void)
fun logOrder(order: Order) {
println("Processing ${order.id}")
}
(2) Default and Named Parameters
// Default parameters - eliminates method overloading
fun createOrder(
id: String,
total: Double,
status: String = "PENDING",
currency: String = "USD",
taxRate: Double = 0.08
): Order = Order(id, total, status, currency)
// Named arguments - call any parameter by name
val order1 = createOrder("ORD-001", 299.99)
val order2 = createOrder("ORD-002", 1_500.00, taxRate = 0.10)
val order3 = createOrder("ORD-003", 45.50, currency = "EUR", status = "CONFIRMED")
(3) Default Parameters vs Java Method Overloading
| Dimension | Java Overloading | Kotlin Default Parameters |
|---|---|---|
| Code size | N methods | 1 method |
| Maintainability | Change N places for one param | Change one place |
| Call flexibility | Positional order only | Named args, any combination |
| Interop | Java calls directly | Needs @JvmOverloads |
4. Higher-Order Functions
Higher-order functions take functions as parameters or return functions — the cornerstone of Kotlin's functional programming.
(1) Function Types
// Function type syntax
val transform: (Order) -> String = { order -> order.id }
val predicate: (Order) -> Boolean = { it.total > 1_000 }
val aggregator: (Double, Order) -> Double = { acc, order -> acc + order.total }
val supplier: () -> Order = { Order("DEFAULT", 0.0, "PENDING") }
// Function as parameter
fun processOrders(orders: List<Order>, transform: (Order) -> String): List<String> {
return orders.map(transform)
}
(2) Function as Return Value
// Return a function
fun getDiscountStrategy(tier: String): (Double) -> Double = when (tier) {
"VIP" -> { total -> total * 0.80 } // 20% off
"GOLD" -> { total -> total * 0.90 } // 10% off
else -> { total -> total } // No discount
}
val discount = getDiscountStrategy("VIP")
val finalPrice = discount(1_000.00) // 800.0
(3) Higher-Order Function + Lambda Call Chain
flowchart LR
A[Orders] --> B[filter]
B --> C[map]
C --> D[sortedBy]
D --> E[fold]
E --> F[Result]
5. Lambda Expressions
(1) Lambda Syntax
// Full syntax
val total = orders.map({ order: Order -> order.total })
// Type inference
val total = orders.map({ order -> order.total })
// it keyword for single parameter
val total = orders.map({ it.total })
// Trailing lambda convention
val total = orders.map { it.total }
// Multi-parameter lambda
val result = orders.fold(0.0) { acc, order -> acc + order.total }
(2) Lambda Syntax Simplification Levels
| Level | Syntax | Description |
|---|---|---|
| Full | { order: Order -> order.total } |
Explicit type |
| Simplified 1 | { order -> order.total } |
Type inference |
| Simplified 2 | { it.total } |
Single param with it |
| Trailing lambda | orders.map { it.total } |
Lambda outside parentheses |
(3) Common Lambda Patterns
// let: transform and consume
val formatted = order.total.let { "\$$it USD" }
// also: side effect without changing value
val order = createOrder("ORD-001", 299.99).also {
println("Created order: ${it.id}")
}
// apply: configure object
val order = Order("ORD-001", 0.0, "PENDING").apply {
// Can only be used with var properties
}
// run: execute block and return result
val summary = order.run {
"Order $id: \$${total} USD ($status)"
}
6. Trailing Lambda Convention
When a function's last parameter is a lambda, it can be written outside the parentheses — this is Kotlin's most important syntactic sugar.
(1) Basic Usage
// Definition
fun processOrders(orders: List<Order>, callback: (Order) -> Unit) { ... }
// Call: trailing lambda
processOrders(orders) { order ->
println(order.id)
}
// If lambda is the ONLY parameter, omit parentheses
orders.forEach { println(it.id) }
(2) Trailing Lambda as the Foundation of DSLs
// This is how DSLs work
fun order(id: String, init: OrderBuilder.() -> Unit): Order {
val builder = OrderBuilder(id)
builder.init()
return builder.build()
}
// Usage: trailing lambda makes it read like configuration
val myOrder = order("ORD-001") {
total(299.99)
status("CONFIRMED")
customer("Alice")
}
7. Inline Functions
Lambdas compile to anonymous class objects on the JVM, creating new instances every call. inline eliminates this overhead.
(1) inline Basics
// Without inline: Lambda creates an object every call
fun process(order: Order, block: (Order) -> String): String {
return block(order)
}
// With inline: Lambda code is inlined at call site
inline fun process(order: Order, block: (Order) -> String): String {
return block(order)
}
(2) inline Effect Comparison
// After inlining, this:
process(order) { it.id }
// Becomes (conceptually):
run { order.id }
(3) inline vs noinline vs crossinline
| Modifier | Effect | Use Case |
|---|---|---|
inline |
Lambda code inlined at call site | Frequently called lambdas |
noinline |
Prevents inlining (stays as object) | Lambda needs to be stored/passed |
crossinline |
Inlined but forbids non-local return | Lambda in a different execution context |
(4) reified Type Parameters
// Normal: type is erased at runtime
fun <T> isType(value: Any): Boolean = value is T // ERROR
// reified: inline preserves type info
inline fun <reified T> isType(value: Any): Boolean = value is T // OK
isType<String>("Hello") // true
isType<Int>(42) // true
isType<Int>("42") // false
8. Complete Example: OrderProcessor Functional Pipeline
// ============================================
// OrderProcessor - Functional Pipeline
// Feature: Process orders with higher-order functions
// ============================================
data class Order(val id: String, val total: Double, val status: String, val customer: String)
// Generic pipeline processor
inline fun <T, R> List<T>.pipeline(
filter: (T) -> Boolean,
transform: (T) -> R,
crossinline postProcess: (R) -> Unit
): List<R> {
return this.filter(filter).map(transform).also { results ->
results.forEach(postProcess)
}
}
// Discount strategy factory
fun getDiscountStrategy(tier: String): (Double) -> Double = when (tier) {
"VIP" -> { total -> total * 0.80 }
"GOLD" -> { total -> total * 0.90 }
"SILVER" -> { total -> total * 0.95 }
else -> { total -> total }
}
fun main() {
val orders = listOf(
Order("ORD-001", 299.99, "CONFIRMED", "Alice"),
Order("ORD-002", 15_000.00, "CONFIRMED", "Bob"),
Order("ORD-003", 2_500.00, "PENDING", "Charlie"),
Order("ORD-004", 45.50, "CANCELLED", "Alice"),
Order("ORD-005", 8_000.00, "CONFIRMED", "Bob")
)
// Pipeline: filter confirmed -> apply VIP discount -> log
val discount = getDiscountStrategy("VIP")
val processed = orders.pipeline(
filter = { it.status == "CONFIRMED" },
transform = { it.copy(total = discount(it.total)) },
postProcess = { println(" Processed: ${it.id} -> \$${it.total} USD") }
)
// Aggregate results
val totalRevenue = processed.sumOf { it.total }
val avgOrder = processed.map { it.total }.average()
println("\nRevenue: \$$totalRevenue USD | Avg: \$${"%.2f".format(avgOrder)} USD")
// Functional composition with let/also/run
val report = orders
.filter { it.status != "CANCELLED" }
.groupBy { it.customer }
.mapValues { (_, list) ->
list.sumOf { it.total }
}
.also { println("\nRevenue by customer:") }
.map { (customer, total) -> "$customer: \$$total USD" }
.joinToString("\n ")
println(" $report")
}
Output:
Processed: ORD-001 -> $239.992 USD
Processed: ORD-002 -> $12000.0 USD
Processed: ORD-005 -> $6400.0 USD
Revenue: $18639.992 USD | Avg: $6213.330666666667 USD
Revenue by customer:
Alice: $299.99 USD
Bob: $23000.0 USD
Charlie: $2500.0 USD
❓ FAQ
it as the implicit parameter and return behaves differently (lambda return is non-local, anonymous function return is local). Use lambdas in most cases.it vs explicit naming?it when the lambda body is 1-2 lines and context is clear; use explicit names for longer bodies or nested lambdas to improve readability.inline?inline is best for small function bodies where the lambda is directly called. Inlining large functions increases code size. Most standard library higher-order functions are inline.reified be used in regular classes?reified only works on type parameters of inline functions, because inlining allows the compiler to preserve type information at the call site. Regular function type parameters are erased at runtime.let/also/apply/run?let for transforming values, also for side effects, apply for configuring objects (returns this), run for executing a block (returns result). Remember: apply returns this, run returns the lambda result.📖 Summary
- Default parameters + named arguments eliminate Java's method overloading — one function handles all call combinations
- Higher-order functions parameterize "what changes", eliminating duplicate logic
- Lambda syntax goes from full to concise:
{ x:Type -> ... }→{ x -> ... }→{ it -> ... }→ trailing lambda - The trailing lambda convention is the syntactic foundation of Kotlin DSLs
inlineeliminates lambda object allocation overhead;reifiedpreserves generic type informationlet/also/apply/runare commonly used scope functions, each with distinct purposes
📝 Exercises
- Beginner (⭐): Write a
calculateTotalfunction with default parameters and call it with named arguments. Hint:fun calc(price: Double, tax: Double = 0.08) - Intermediate (⭐⭐): Implement
processAndTransformwith higher-order functions, accepting a filter condition and a transform function, returning a result list. Hint:<T> fun List<T>.process... - Challenge (⭐⭐⭐): Implement a
filterByType<Any>function that filters elements of a specified type from a mixed-type list. Hint:inline fun <reified T> List<Any>.filterByType(): List<T>



