Higher-Order Functions: Lambdas as Parameters and Return Values

Kotlin

2026-08-21 09:00

Where we are

From using to writing

Programming with Lambdas taught you to use filter and map.

This unit is about writing them.

And about the cost

In Java, every lambda is an object, and calling it is an indirection.

In a hot loop, that is a real reason to write the imperative version.

Kotlin’s answer

inline — and it is more thorough than it sounds.

The compiler substitutes the function’s body and the lambda’s body.

No object. No call. Nothing left of the abstraction.

What you will be able to do

  1. Write function types.
  2. Declare and call higher-order functions.
  3. Default or nullable function parameters.
  4. Return functions from functions.
  5. Remove duplication with a lambda parameter.
  6. Explain what inline does.
  7. Judge when to inline.
  8. Use use and withLock.
  9. Control returns from lambdas.

Function types

The syntax

Writing them down

val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println(42) }

The return type is always written, even when it is Unit.

The arrow needs something after it.

Nullable, with care

var canReturnNull: (Int, Int) -> Int? = { x, y -> null }   // returns Int?
var funOrNull: ((Int, Int) -> Int)? = null                 // may be null

Without the outer parentheses, the ? attaches to the return type.

How this differs from unit 5

There: a SAM conversion into a Java interface instance.

Here: a proper type. No conversion happens or is needed.

Underneath it compiles to Function1 — so Java can still call it.

Taking functions as parameters

The declaration

Calling it

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

twoAndThree { a, b -> a + b }      // 5
twoAndThree { a, b -> a * b }      // 6

Name the parameters inside the type

(element: T) -> Boolean

The IDE shows those names at the call site.

Real documentation for whoever writes the lambda.

Write filter yourself

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}

Doing this once changes how the standard library reads.

Optional: a default value

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        transform: (T) -> String = { it.toString() }
): String
letters.joinToString()                        // uses toString
letters.joinToString { it.toLowerCase() }     // supplies its own

Optional: a nullable type

transform: ((T) -> String)? = null

val str = transform?.invoke(element) ?: element.toString()

Which to use

Default value, usually. The caller sees a working default; the body has no null handling.

Nullable, when absence is genuinely meaningful.

Note the name invoke

Another convention, from unit 7.

Calling a value with parentheses means calling its invoke.

Which is why ?.invoke() is the safe-call form.

Returning functions

Built from runtime state

fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        return { order -> 6 + 2.1 * order.itemCount }
    }
    return { order -> 1.2 * order.itemCount }
}

The returned lambda captures the enclosing parameters.

The persuasive example

fun getPredicate(): (Person) -> Boolean {
    val startsWithPrefix = { p: Person -> p.firstName.startsWith(prefix) }
    if (!onlyWithPhoneNumber) return startsWithPrefix
    return { startsWithPrefix(it) && it.phoneNumber != null }
}

contacts.filter(contactListFilters.getPredicate())

Compare with the alternative

By hand: a chain of ifs inside the filtering loop.

Re-evaluated per element.

Composing a function moves the logic to one place.

Removing duplication

Three near-identical functions

Average duration for Windows users.

Then for Mac users.

Then filtered by page.

One

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
    filter(predicate).map(SiteVisit::duration).average()

log.averageDurationFor { it.os == OS.WINDOWS }
log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) }
log.averageDurationFor { it.os == OS.IOS && it.path == "/signup" }

Why a lambda rather than a class

Java’s answer: a strategy interface with an implementation per case.

Several files for what is here one parameter.

And the caller writes the behaviour inline, where a reader can see it.

The general lesson

In Kotlin, “extract the difference” often means extracting a function.

In Java it would mean extracting a type.

Inline functions

What inline does

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try { return action() } finally { lock.unlock() }
}

The call site

synchronized(l) { println("Action") }

becomes

l.lock()
try { println("Action") } finally { l.unlock() }

No object anywhere.

Where it does not apply

If a lambda parameter is stored or passed on,

its body cannot be substituted — something is holding it.

noinline exempts that parameter; the rest still inlines.

When to use it

Small functions taking lambdas — which is why filter costs no more than a loop.

Not large bodies — the body is copied into every call site.

Two different optimisations

inline removes the lambda overhead.

It does not remove the intermediate collections.

That is what sequences were for. Different costs, different tools.

Resource management

The pattern

fun readFirstLineFromFile(path: String): String {
    BufferedReader(FileReader(path)).use { br ->
        return br.readLine()
    }
}

Acquire, run, release in a finally.

Java added syntax

try-with-resources — a language feature, in the compiler.

Kotlin needs none. use is an ordinary extension function.

And it costs nothing

Because it is inline.

Without that, every use would allocate.

Free higher-order functions are what let a library do a language’s job.

Note that return

The return inside the use block

returns from readFirstLineFromFile, not from the lambda.

Which is the next section — and only possible because use is inline.

Returns from lambdas

Non-local return

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return                    // returns from lookForAlice
        }
    }
    println("Alice is not among them")
}

Which is what you want

forEach with a return behaves exactly like a for loop with a return.

That is what makes the functional version a genuine replacement.

Why only when inlined

The lambda’s body has been compiled into the enclosing function.

So the return is an ordinary return from that function.

A non-inlined lambda lives in a separate object. The compiler forbids it.

Labels

Returning from the lambda alone

people.forEach label@{
    if (it.name != "Alice") return@label
}

people.forEach {
    if (it.name != "Alice") return@forEach
}

The lambda’s equivalent of continue.

Anonymous functions

people.forEach(fun (person) {
    if (person.name == "Alice") return        // returns from this
    println("${person.name} is not Alice")
})

One rule resolves all three

return returns from the nearest enclosing function declared with fun.

In a lambda, that is the outer function.

In an anonymous function, it is the anonymous function.

Which to choose

Lambdas for nearly everything. They read better.

An anonymous function when several returns would each need a label.

Summary

The six things to carry away

  • Higher-order functions are free, and that changes what a library can do.
  • Which is why use is a function where Java needed syntax.
  • Write filter once and the standard library stops being magic.
  • Extract a function where Java would extract a type.
  • A default lambda beats a nullable function parameter most of the time.
  • Non-local return is a consequence of inlining, not a rule.

Where next

Generics — type parameters, constraints, and erasure.

Kotlin’s answer to erasure is reified type parameters.

Which work only because of inlining. One more thing this unit paid for in advance.