Skip to main content

Higher-order functions

A higher-order function accepts a function, returns a function, or does both.

fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int =
operation(a, b)

val result = calculate(10, 5) { left, right -> left + right }

calculate controls when the operation runs. The caller supplies what the operation does.

This is useful when the steps stay the same but one rule changes. calculate always receives two numbers and invokes one operation; the caller chooses addition, subtraction, or another calculation.

Open in Kotlin Playground
Expected: 15, followed by 5.

Accepting behavior as a parameter​

fun <T> findFirst(items: List<T>, matches: (T) -> Boolean): T? {
for (item in items) {
if (matches(item)) return item
}
return null
}

The function owns iteration. The caller provides the rule:

val firstLongName = findFirst(names) { name -> name.length > 6 }

This reduces duplicated control flow while keeping the changing rule visible.

Execution of findFirst(names) { name -> name.length > 6 }:

  1. findFirst starts at the first item.
  2. It calls matches(item) for that item.
  3. It returns immediately when the predicate is true.
  4. It returns null only if every item fails the test.

This short-circuit behavior matters: items after the first match are never tested.

Returning behavior​

fun minimumLengthValidator(minimum: Int): (String) -> Boolean =
{ text -> text.length >= minimum }

val validPassword = minimumLengthValidator(8)
println(validPassword("android")) // true

The returned lambda remembers minimum. This is a closure: a function value plus the outside values it captured.

Open in Kotlin Playground
Expected: true, followed by false. The returned function remembers that minimum is 8.

Collection operations are higher-order functions​

val visibleNames = users
.filter { user -> user.isActive }
.sortedBy { user -> user.name }
.map { user -> user.name }
  • filter keeps matching items.
  • map transforms every item.
  • sortedBy chooses the value used for ordering.
  • any, all, and none answer questions about a collection.
  • fold combines items into one accumulated result.

Avoid long chains that hide important decisions or create many intermediate collections in a performance-sensitive path. A clear loop is sometimes the better code.

Open in Kotlin Playground
Expected: [Grace, Rakesh].

Callback design​

fun loadUser(
id: Long,
onSuccess: (User) -> Unit,
onError: (Throwable) -> Unit,
) { /* start work */ }

Callbacks are still useful for UI events and adapting callback-based APIs. For asynchronous operations that produce one result, a suspend function is often easier to compose and cancel than multiple callbacks.

The loadUser snippet is intentionally incomplete because thread choice, cancellation, and the actual data source depend on the Android application. It demonstrates the callback API shape; it is not presented as runnable Playground code.

Function composition​

fun <A, B, C> compose(
first: (A) -> B,
second: (B) -> C,
): (A) -> C = { value -> second(first(value)) }

Composition is useful when each transformation has one clear job. Do not build an abstract pipeline when a direct function call is easier to follow.

Open in Kotlin Playground
Expected: 7. The string is trimmed first; the resulting string is then measured.

Corner cases​

A function parameter may be nullable​

fun notify(message: String, listener: ((String) -> Unit)?) {
listener?.invoke(message)
}

Use a nullable callback only when “no listener” is a valid state. If the behavior is required, keep the parameter non-null so callers must provide it.

Open in Kotlin Playground
Expected: Received: Saved, followed by Finished. The null listener performs no action.
Open in Kotlin Playground
Expected: the active User(name=Rakesh, active=true), followed by true.

Common mistakes​

1. Several callbacks make the call site confusing​

✕ Avoid unnamed meaning

loadUser(42, { show(it) }, { log(it) }, { hideLoading() })

✓ Name every behavior

loadUser(
id = 42,
onSuccess = { user -> show(user) },
onError = { error -> log(error) },
onComplete = { hideLoading() },
)

If the asynchronous flow keeps growing, prefer a suspend function or Flow instead of adding more callbacks.

2. A collection chain creates unnecessary intermediate lists​

✕ Wasteful for a large list

val firstName = users
.filter { it.active }
.map { it.name }
.firstOrNull()

✓ Stop as soon as a match is found

val firstName = users.firstOrNull { user -> user.active }?.name

The first version can build filtered and mapped lists. The second searches until it finds one active user.

3. A generic abstraction hides simple business logic​

✕ Unnecessary abstraction

fun <T, R> execute(value: T, operation: (T) -> R): R = operation(value)

val total = execute(price) { it * quantity }

✓ Say what the business operation does

fun orderTotal(price: Double, quantity: Int): Double = price * quantity

Use a higher-order function when it removes repeated control flow or creates a genuinely useful API—not merely because Kotlin allows it.

Interview-ready answer

A higher-order function accepts or returns another function. Kotlin collection operators such as map and filter are common examples. They help separate stable control flow from caller-specific behavior, but too many callbacks or transformations can make execution and performance harder to understand.