Kotlin with Copilot: Higher order add function
Video: Kotlin with Copilot: Higher order add function by Taught by Celeste AI - AI Coding Coach
Kotlin: Higher-Order Functions — Pass an Operation as a Parameter
A function that accepts another function as a parameter is higher-order.
fun apply(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b). Pass+,-,*as values.
A small puzzle that demos Kotlin's first-class function support — the foundation of map, filter, callbacks, and event handlers.
The Copilot prompt
// A higher-order function that takes an operation and applies it to two integers
fun higherOrderAdd(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
Copilot generates:
fun higherOrderAdd(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun main() {
val add: (Int, Int) -> Int = { x, y -> x + y }
val result = higherOrderAdd(5, 10, add)
println("Result: $result") // 15
}
Walkthrough
1. The function-type parameter. operation: (Int, Int) -> Int — a function taking two Ints and returning an Int. Just like a normal parameter type, except it describes a callable.
2. The body. operation(a, b) — invoke the function passed in. Same syntax as calling any function.
3. The lambda. { x, y -> x + y } is an anonymous function. The (Int, Int) -> Int type annotation tells Kotlin what type the lambda is.
4. The call. higherOrderAdd(5, 10, add) — three arguments: two ints and one function.
Function types
Kotlin's function-type syntax: (P1, P2, ...) -> R. Examples:
() -> Unit— no params, no return.(Int) -> Int— takes Int, returns Int.(String) -> Boolean— takes String, returns Boolean (a predicate).(Int, Int) -> Int— takes two Ints, returns Int.
These are real types — var f: (Int) -> Int = { x -> x * 2 }. You can store them, pass them, return them.
Function references
For named functions, ::name produces a function value:
fun add(x: Int, y: Int): Int = x + y
fun subtract(x: Int, y: Int): Int = x - y
val addRef: (Int, Int) -> Int = ::add
println(higherOrderAdd(5, 10, ::add)) // 15
println(higherOrderAdd(5, 10, ::subtract)) // -5
::add is a function reference. Equivalent to { x, y -> add(x, y) } but cheaper — no lambda allocation.
Passing different operations
val add: (Int, Int) -> Int = { x, y -> x + y }
val subtract: (Int, Int) -> Int = { x, y -> x - y }
val multiply: (Int, Int) -> Int = { x, y -> x * y }
println(higherOrderAdd(5, 10, add)) // 15
println(higherOrderAdd(5, 10, subtract)) // -5
println(higherOrderAdd(5, 10, multiply)) // 50
Same higher-order function, three different behaviors via different lambdas.
Returning a function
A function can also return a function:
fun makeMultiplier(factor: Int): (Int) -> Int {
return { x -> x * factor }
}
val double = makeMultiplier(2)
val triple = makeMultiplier(3)
println(double(5)) // 10
println(triple(5)) // 15
makeMultiplier returns a closure that captures factor. Each call to makeMultiplier produces a new function with its own factor.
This is the factory pattern in functional clothes. Episode 16 of the Lua series covers the same idea.
Trailing-lambda syntax
When the last parameter is a function, Kotlin lets you write the lambda outside the parentheses:
val result = higherOrderAdd(5, 10) { x, y -> x + y }
Compare to:
val result = higherOrderAdd(5, 10, { x, y -> x + y })
Same call. The trailing-lambda form reads more naturally — most Kotlin code uses it.
This is why numbers.filter { it > 0 } works — filter takes one argument (the predicate); the lambda goes after the () (which is omitted when there are no other args).
inline for performance
Higher-order functions allocate a function object per call. For hot paths, mark the function inline:
inline fun higherOrderAdd(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
The compiler inlines the body at every call site, so no function object is allocated. Used everywhere in stdlib (map, filter, also, apply, etc.).
Don't inline huge functions — bytecode duplication. Reserve for small, frequently-called HOFs.
Common patterns: callbacks
fun loadUser(id: Int, onSuccess: (User) -> Unit, onError: (Throwable) -> Unit) {
// ... async work ...
if (...) onSuccess(user) else onError(error)
}
loadUser(
id = 42,
onSuccess = { user -> println("Loaded: $user") },
onError = { e -> println("Failed: ${e.message}") },
)
Two function parameters, one for each outcome. Common in async APIs before suspend functions took over.
Standard library: fold, reduce, map, filter
The Kotlin stdlib is built on higher-order functions:
val sum = (1..10).fold(0) { acc, n -> acc + n } // 55
val max = listOf(3, 1, 4, 1, 5).reduce { a, b -> if (a > b) a else b } // 5
val doubled = listOf(1, 2, 3).map { it * 2 } // [2, 4, 6]
val even = listOf(1, 2, 3, 4).filter { it % 2 == 0 } // [2, 4]
Each takes a function as an argument. The whole functional style of Kotlin is built on this.
Common mistakes
Calling instead of referencing. higherOrderAdd(5, 10, add(1, 2)) is wrong — that calls add first and passes its result (an Int, not a function). Pass add, not add(...).
Wrong function-type signature. (Int) -> Int for a 2-arg function won't compile. Match the parameter list exactly.
Forgetting inline for hot paths. Adds millions of lambdas/sec on inner loops.
return inside a lambda. Returns from the enclosing function, not from the lambda. Use a labelled return: return@filter to return from a filter lambda.
Confusing ::function with function(). ::name produces a function value. name() calls the function. Different semantics.
What's next
Episode 26: Lambda to print a full name from first + last. A two-arg lambda that produces side effects.
Recap
Higher-order functions take or return functions. Function type syntax: (P1, P2) -> R. Lambdas: { x, y -> x + y }. Function references: ::name. Trailing-lambda syntax f(args) { ... } is idiomatic. inline to avoid lambda allocation in hot paths. The whole Kotlin stdlib (map, filter, fold, reduce) is HOFs.
Next episode: lambda for full name.