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) -> Rto accept transformation logic. - Prefer extension functions such as
Iterable<T>.myTransform. - Use generics to make transformations reusable.
- Use
inlinefor small higher-order functions that are called often. - Use
Sequencefor lazy transformations on large or chained data pipelines. - Use
foldwhen your transformation naturally accumulates a result. - For many cases, Kotlin’s built-ins like
map,filter,flatMap,mapNotNull, andgroupByare already ideal. Custom transformations are most useful when they express domain-specific behavior clearly.
