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.