How do I build custom collection transformations using higher-order functions in Kotlin?

In Kotlin, custom collection transformations are often built with higher-order functions: functions that take other functions as parameters (lambdas) or return functions.

Kotlin’s standard library already has transformations like map, filter, flatMap, groupBy, and fold, but you can build your own reusable transformations when your logic becomes domain-specific.

1. Basic higher-order transformation

A simple custom transformation can accept a collection and a lambda:

fun <T, R> transformAll(items: List<T>, transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()

    for (item in items) {
        result.add(transform(item))
    }

    return result
}

Usage:

val numbers = listOf(1, 2, 3)

val doubled = transformAll(numbers) { it * 2 }

println(doubled) // [2, 4, 6]

This is essentially a simplified custom version of map.

2. Prefer extension functions for collection APIs

In Kotlin, collection transformations are usually more natural as extension functions:

fun <T, R> Iterable<T>.customMap(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()

    for (item in this) {
        result.add(transform(item))
    }

    return result
}

Usage:

val names = listOf("alice", "bob", "charlie")

val uppercased = names.customMap { it.uppercase() }

println(uppercased) // [ALICE, BOB, CHARLIE]

3. Building a custom filter-map transformation

Sometimes you want to transform values and skip invalid results. You can use a lambda that returns nullable values:

fun <T, R : Any> Iterable<T>.mapNotNullCustom(
    transform: (T) -> R?
): List<R> {
    val result = mutableListOf<R>()

    for (item in this) {
        val transformed = transform(item)
        if (transformed != null) {
            result.add(transformed)
        }
    }

    return result
}

Usage:

val inputs = listOf("1", "abc", "42", "x")

val numbers = inputs.mapNotNullCustom { it.toIntOrNull() }

println(numbers) // [1, 42]

Kotlin already has mapNotNull, but this pattern is useful when designing domain-specific transformations.

4. Add predicates for conditional transformations

You can combine filtering and mapping in one custom function:

fun <T, R> Iterable<T>.mapIf(
    predicate: (T) -> Boolean,
    transform: (T) -> R
): List<R> {
    val result = mutableListOf<R>()

    for (item in this) {
        if (predicate(item)) {
            result.add(transform(item))
        }
    }

    return result
}

Usage:

val numbers = listOf(1, 2, 3, 4, 5)

val evenSquares = numbers.mapIf(
    predicate = { it % 2 == 0 },
    transform = { it * it }
)

println(evenSquares) // [4, 16]

5. Use inline for performance-sensitive transformations

Higher-order functions often allocate function objects. For collection utilities, Kotlin commonly uses inline to reduce overhead:

inline fun <T, R> Iterable<T>.customMapInline(
    transform: (T) -> R
): List<R> {
    val result = mutableListOf<R>()

    for (item in this) {
        result.add(transform(item))
    }

    return result
}

Usage is the same:

val words = listOf("a", "bb", "ccc")

val lengths = words.customMapInline { it.length }

println(lengths) // [1, 2, 3]

Use inline when:

  • the function is small,
  • it accepts lambdas,
  • it may be called frequently,
  • you want to avoid lambda allocation overhead.

6. Transform with index

You can also include the element index:

inline fun <T, R> Iterable<T>.customMapIndexed(
    transform: (index: Int, value: T) -> R
): List<R> {
    val result = mutableListOf<R>()
    var index = 0

    for (item in this) {
        result.add(transform(index, item))
        index++
    }

    return result
}

Usage:

val letters = listOf("a", "b", "c")

val labeled = letters.customMapIndexed { index, value ->
    "$index:$value"
}

println(labeled) // [0:a, 1:b, 2:c]

7. Custom transformations using fold

Many transformations can be built using fold:

fun <T, R> Iterable<T>.customTransformWithFold(
    initial: R,
    operation: (accumulator: R, item: T) -> R
): R {
    return this.fold(initial, operation)
}

Usage:

val numbers = listOf(1, 2, 3, 4)

val sumOfSquares = numbers.customTransformWithFold(0) { acc, item ->
    acc + item * item
}

println(sumOfSquares) // 30

You can also build a map-like function with fold:

fun <T, R> Iterable<T>.mapUsingFold(
    transform: (T) -> R
): List<R> {
    return this.fold(mutableListOf<R>()) { acc, item ->
        acc.add(transform(item))
        acc
    }
}

8. Domain-specific transformation example

Suppose you have orders and want a reusable transformation for valid orders:

data class Order(
    val id: String,
    val amount: Double,
    val status: String
)

data class OrderSummary(
    val id: String,
    val amount: Double
)

inline fun Iterable<Order>.toSummariesIf(
    predicate: (Order) -> Boolean
): List<OrderSummary> {
    val result = mutableListOf<OrderSummary>()

    for (order in this) {
        if (predicate(order)) {
            result.add(
                OrderSummary(
                    id = order.id,
                    amount = order.amount
                )
            )
        }
    }

    return result
}

Usage:

val orders = listOf(
    Order("A001", 120.0, "PAID"),
    Order("A002", 80.0, "PENDING"),
    Order("A003", 250.0, "PAID")
)

val paidSummaries = orders.toSummariesIf { it.status == "PAID" }

println(paidSummaries)
// [OrderSummary(id=A001, amount=120.0), OrderSummary(id=A003, amount=250.0)]

9. Lazy transformations with Sequence

If you are processing large collections or chaining multiple transformations, consider using Sequence:

fun <T, R> Sequence<T>.customMapLazy(
    transform: (T) -> R
): Sequence<R> = sequence {
    for (item in this@customMapLazy) {
        yield(transform(item))
    }
}

Usage:

val result = sequenceOf(1, 2, 3, 4, 5)
    .customMapLazy { it * 2 }
    .filter { it > 5 }
    .toList()

println(result) // [6, 8, 10]

This avoids creating intermediate collections.

10. Practical reusable pattern

A good general-purpose custom transformation often looks like this:

inline fun <T, R> Iterable<T>.transformWhere(
    predicate: (T) -> Boolean,
    transform: (T) -> R
): List<R> {
    val result = mutableListOf<R>()

    for (item in this) {
        if (predicate(item)) {
            result.add(transform(item))
        }
    }

    return result
}

Usage:

val users = listOf("alice", "bob", "anna", "charlie")

val namesStartingWithA = users.transformWhere(
    predicate = { it.startsWith("a") },
    transform = { it.replaceFirstChar(Char::uppercase) }
)

println(namesStartingWithA) // [Alice, Anna]

Key takeaways

  • Use function types like (T) -> R to accept transformation logic.
  • Prefer extension functions such as Iterable<T>.myTransform.
  • Use generics to make transformations reusable.
  • Use inline for small higher-order functions that are called often.
  • Use Sequence for lazy transformations on large or chained data pipelines.
  • Use fold when your transformation naturally accumulates a result.
  • For many cases, Kotlin’s built-ins like map, filter, flatMap, mapNotNull, and groupBy are already ideal. Custom transformations are most useful when they express domain-specific behavior clearly.

How do I use sequences to optimize performance with large Kotlin collections?

In Kotlin, sequences let you process large collections lazily, which can reduce temporary allocations and improve performance for chained operations.

The problem with regular collections

Collection operations like map, filter, and flatMap are usually eager:

val result = users
    .filter { it.isActive }
    .map { it.email }
    .take(10)

With a List, Kotlin typically creates intermediate collections:

  1. one list after filter
  2. another list after map
  3. then takes 10 items

For small collections, this is fine. For large collections, it can waste memory and CPU.

Use asSequence()

Convert the collection to a sequence:

val result = users
    .asSequence()
    .filter { it.isActive }
    .map { it.email }
    .take(10)
    .toList()

Now each element flows through the pipeline one at a time:

user -> filter -> map -> maybe included in result

Also, because of take(10), processing can stop as soon as 10 matching items are found.

Example

data class User(
    val name: String,
    val email: String,
    val isActive: Boolean
)

val emails = users
    .asSequence()
    .filter { it.isActive }
    .map { it.email.lowercase() }
    .distinct()
    .take(100)
    .toList()

This avoids building large intermediate lists before getting the final 100 emails.

When sequences help

Sequences are usually useful when you have:

  • large collections
  • multiple chained operations
  • short-circuiting operations, such as:
    • take
    • first
    • firstOrNull
    • any
    • all
    • none
    • find

Example:

val firstAdminEmail = users
    .asSequence()
    .filter { it.isActive }
    .filter { it.role == "admin" }
    .map { it.email }
    .firstOrNull()

This stops as soon as the first matching user is found.

When sequences may not help

Sequences are not always faster. They can be slower for:

  • small collections
  • simple one-step operations
  • cases where you need the entire result anyway
  • performance-critical tight loops where sequence overhead matters

For example, this may not benefit much:

val names = users.map { it.name }

A plain collection operation is often fine here.

Common pattern

Use this pattern:

val result = largeList
    .asSequence()
    .filter { condition(it) }
    .map { transform(it) }
    .take(50)
    .toList()

The important parts are:

  • asSequence() starts lazy processing
  • intermediate operations stay lazy
  • terminal operations execute the pipeline

Terminal operations include:

toList()
count()
first()
firstOrNull()
sumOf { ... }
any { ... }
forEach { ... }

Rule of thumb

Use sequences when your pipeline is large, chained, and potentially short-circuited.

For small or simple transformations, prefer normal collection operations for readability and often better performance.

How do I use immutability and copy-on-write with Kotlin collections?

In Kotlin, read-only collection interfaces plus copy-on-write updates are a common way to model immutable-style data.

Kotlin has two main collection interface families:

Mutable Read-only
MutableList<T> List<T>
MutableSet<T> Set<T>
MutableMap<K, V> Map<K, V>

Read-only interfaces prevent mutation through that reference, while copy-on-write means you create a new collection when you need a changed version.

Use read-only collection types

Prefer exposing List, Set, and Map instead of mutable versions:

class UserGroup(
    val users: List<String>
)

val group = UserGroup(listOf("Alice", "Bob"))

// group.users.add("Charlie") // Does not compile

This prevents callers from mutating the collection directly.

Copy-on-write with lists

To “modify” a list, create a new one:

val users = listOf("Alice", "Bob")

val withCharlie = users + "Charlie"
val withoutAlice = users - "Alice"

println(users)        // [Alice, Bob]
println(withCharlie)  // [Alice, Bob, Charlie]
println(withoutAlice) // [Bob]

The original list remains unchanged.

Copy-on-write with sets

val roles = setOf("USER", "ADMIN")

val updatedRoles = roles + "AUDITOR"
val reducedRoles = roles - "ADMIN"

println(roles)        // [USER, ADMIN]
println(updatedRoles) // [USER, ADMIN, AUDITOR]
println(reducedRoles) // [USER]

Copy-on-write with maps

val settings = mapOf(
    "theme" to "dark",
    "language" to "en"
)

val updatedSettings = settings + ("theme" to "light")
val removedSetting = settings - "language"

println(settings)
// {theme=dark, language=en}

println(updatedSettings)
// {theme=light, language=en}

println(removedSetting)
// {theme=dark}

Updating objects with immutable collections

This pattern is especially useful with data class and copy():

data class Team(
    val name: String,
    val members: List<String>
)

val team = Team(
    name = "Backend",
    members = listOf("Alice", "Bob")
)

val updatedTeam = team.copy(
    members = team.members + "Charlie"
)

println(team)
// Team(name=Backend, members=[Alice, Bob])

println(updatedTeam)
// Team(name=Backend, members=[Alice, Bob, Charlie])

Avoid leaking mutable collections

Be careful with this:

val mutableUsers = mutableListOf("Alice", "Bob")
val users: List<String> = mutableUsers

mutableUsers += "Charlie"

println(users) // [Alice, Bob, Charlie]

Even though users is typed as List<String>, the underlying collection is still mutable.

If you need a defensive copy:

val users: List<String> = mutableUsers.toList()

Encapsulate mutation internally

A common pattern is to keep a mutable collection private, but expose a read-only copy:

class UserRegistry {
    private val users = mutableListOf<String>()

    fun addUser(user: String) {
        users += user
    }

    fun users(): List<String> = users.toList()
}

This prevents external code from modifying your internal list.

Prefer immutable state updates

For state management, prefer this style:

data class AppState(
    val selectedIds: Set<Int>,
    val usersById: Map<Int, String>
)

val state = AppState(
    selectedIds = setOf(1, 2),
    usersById = mapOf(
        1 to "Alice",
        2 to "Bob"
    )
)

val newState = state.copy(
    selectedIds = state.selectedIds + 3,
    usersById = state.usersById + (3 to "Charlie")
)

Use persistent collections for heavy updates

Kotlin’s standard collections are read-only interfaces, but not deeply immutable persistent data structures.

If you frequently create modified copies of large collections, consider kotlinx.collections.immutable:

import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf

val users: PersistentList<String> = persistentListOf("Alice", "Bob")

val updatedUsers = users.add("Charlie")

println(users)        // [Alice, Bob]
println(updatedUsers) // [Alice, Bob, Charlie]

Persistent collections share internal structure, so copy-on-write-style updates are usually more efficient than copying a whole List.

Summary

Use:

val names: List<String> = listOf("Alice", "Bob")
val updated = names + "Charlie"

Instead of:

val names = mutableListOf("Alice", "Bob")
names += "Charlie"

General guidance:

  • Expose List, Set, and Map, not mutable types.
  • Use +, -, map, filter, and copy() to create updated values.
  • Avoid leaking mutable backing collections.
  • Use toList(), toSet(), or toMap() for defensive copies.
  • Use kotlinx.collections.immutable for efficient persistent immutable collections.

How do I combine multiple collection operations in a single Kotlin chain?

In Kotlin, you can combine multiple collection operations by chaining functions like filter, map, sortedBy, take, groupBy, and others.

Each operation returns a new collection, so you can call the next operation directly on the result.

val numbers = listOf(1, 2, 3, 4, 5, 6)

val result = numbers
    .filter { it % 2 == 0 }
    .map { it * 10 }
    .sorted()

println(result) // [20, 40, 60]

Here’s what happens:

  1. filter { it % 2 == 0 } keeps only even numbers
  2. map { it * 10 } transforms each number
  3. sorted() sorts the result

You can also chain operations on objects:

data class User(
    val name: String,
    val age: Int,
    val active: Boolean
)

val users = listOf(
    User("Alice", 30, true),
    User("Bob", 17, true),
    User("Charlie", 25, false),
    User("Diana", 22, true)
)

val activeAdultNames = users
    .filter { it.active }
    .filter { it.age >= 18 }
    .map { it.name }
    .sorted()

println(activeAdultNames) // [Alice, Diana]

You can often combine related filters into one:

val activeAdultNames = users
    .filter { it.active && it.age >= 18 }
    .map { it.name }
    .sorted()

For maps, you can chain over entries:

val scores = mapOf(
    "Alice" to 90,
    "Bob" to 75,
    "Charlie" to 85
)

val passedNames = scores
    .filter { (_, score) -> score >= 80 }
    .map { (name, _) -> name }
    .sorted()

println(passedNames) // [Alice, Charlie]

If the collection is large or the chain is expensive, use asSequence() to make intermediate operations lazy:

val result = numbers
    .asSequence()
    .filter { it % 2 == 0 }
    .map { it * 10 }
    .sorted()
    .toList()

Use regular collection chains for simple cases, and asSequence() when you want to avoid creating intermediate collections during multi-step processing.

How do I use destructuring with maps and pairs in Kotlin?

In Kotlin, destructuring lets you unpack values from an object into separate variables.

It is commonly used with:

  • Pair
  • Map.Entry
  • loops over maps
  • lambda parameters

Destructuring a Pair

A Pair<A, B> contains two values: first and second.

val pair = "Alice" to 25

val (name, age) = pair

println(name) // Alice
println(age)  // 25

This is equivalent to:

val name = pair.first
val age = pair.second

You can also create a Pair explicitly:

val pair = Pair("Kotlin", 2.1)

val (language, version) = pair

println(language) // Kotlin
println(version)  // 2.1

Destructuring map entries in a loop

When you iterate over a map, each item is a Map.Entry<K, V>. Kotlin lets you destructure it into key and value:

val ages = mapOf(
    "Alice" to 25,
    "Bob" to 30
)

for ((name, age) in ages) {
    println("$name is $age years old")
}

Here:

(name, age)

means:

name = entry.key
age = entry.value

Destructuring in lambdas

You can destructure map entries inside lambda parameters too:

val scores = mapOf(
    "Alice" to 90,
    "Bob" to 85,
    "Charlie" to 95
)

scores.forEach { (name, score) ->
    println("$name scored $score")
}

You can also use it with operations like map, filter, and sortedBy:

val passingStudents = scores.filter { (_, score) ->
    score >= 90
}

println(passingStudents) // {Alice=90, Charlie=95}

Ignoring values with _

If you only need one part of the destructured value, use _ for the part you want to ignore.

val user = "Alice" to 25

val (name, _) = user

println(name) // Alice

With a map:

val ages = mapOf(
    "Alice" to 25,
    "Bob" to 30
)

for ((name, _) in ages) {
    println(name)
}

Or if you only need values:

for ((_, age) in ages) {
    println(age)
}

Destructuring mutable maps

Destructuring works the same way with mutable maps:

val inventory = mutableMapOf(
    "Apples" to 10,
    "Oranges" to 5
)

for ((item, count) in inventory) {
    println("$item: $count")
}

If you want to update values, use the key:

for ((item, count) in inventory) {
    inventory[item] = count + 1
}

println(inventory) // {Apples=11, Oranges=6}

Important detail: destructuring depends on componentN() functions

Destructuring works when a type provides functions like:

component1()
component2()

Pair has:

component1() // first
component2() // second

Map entries support destructuring as:

component1() // key
component2() // value

So this:

val (key, value) = "id" to 123

is essentially:

val pair = "id" to 123
val key = pair.component1()
val value = pair.component2()

Quick summary

val pair = "Alice" to 25
val (name, age) = pair

val map = mapOf(
    "Alice" to 25,
    "Bob" to 30
)

for ((key, value) in map) {
    println("$key -> $value")
}

map.forEach { (key, value) ->
    println("$key -> $value")
}

Use destructuring when it makes key-value or pair-based code easier to read.