How do I implement tail-recursive algorithms with collections in Kotlin?

To implement tail-recursive algorithms with collections in Kotlin, structure your function so that:

  1. The recursive call is the last operation
  2. Any intermediate result is carried in an accumulator
  3. The collection is processed by index, iterator-like state, or remaining sublist
  4. You mark the function with tailrec

Basic pattern

tailrec fun process(
    items: List<Int>,
    index: Int = 0,
    acc: Int = 0
): Int {
    return if (index == items.size) {
        acc
    } else {
        process(items, index + 1, acc + items[index])
    }
}

The recursive call to process(...) is the final operation, so Kotlin can optimize it into a loop.

Example: sum a list

tailrec fun sum(
    numbers: List<Int>,
    index: Int = 0,
    acc: Int = 0
): Int {
    return if (index == numbers.size) {
        acc
    } else {
        sum(numbers, index + 1, acc + numbers[index])
    }
}

fun main() {
    println(sum(listOf(1, 2, 3, 4))) // 10
}

Here, acc stores the running total.

Example: find an element

tailrec fun <T> containsItem(
    items: List<T>,
    target: T,
    index: Int = 0
): Boolean {
    return when {
        index == items.size -> false
        items[index] == target -> true
        else -> containsItem(items, target, index + 1)
    }
}

fun main() {
    val names = listOf("Ada", "Grace", "Linus")

    println(containsItem(names, "Grace")) // true
    println(containsItem(names, "Kotlin")) // false
}

This is tail-recursive because each branch either returns a value directly or calls the function as the last action.

Example: map a collection

For transformations, use an accumulator collection.

tailrec fun <T, R> mapTailrec(
    items: List<T>,
    transform: (T) -> R,
    index: Int = 0,
    acc: MutableList<R> = mutableListOf()
): List<R> {
    return if (index == items.size) {
        acc
    } else {
        acc.add(transform(items[index]))
        mapTailrec(items, transform, index + 1, acc)
    }
}

fun main() {
    val result = mapTailrec(listOf(1, 2, 3)) { it * 2 }
    println(result) // [2, 4, 6]
}

This works, but note that it uses a mutable accumulator internally.

If you want a public immutable-looking API, wrap it:

fun <T, R> List<T>.mapTailrec(transform: (T) -> R): List<R> {
    tailrec fun loop(
        index: Int,
        acc: MutableList<R>
    ): List<R> {
        return if (index == size) {
            acc
        } else {
            acc.add(transform(this[index]))
            loop(index + 1, acc)
        }
    }

    return loop(0, ArrayList(size))
}

Usage:

val doubled = listOf(1, 2, 3).mapTailrec { it * 2 }
println(doubled) // [2, 4, 6]

Example: filter a collection

fun <T> List<T>.filterTailrec(predicate: (T) -> Boolean): List<T> {
    tailrec fun loop(
        index: Int,
        acc: MutableList<T>
    ): List<T> {
        if (index == size) {
            return acc
        }

        val item = this[index]

        if (predicate(item)) {
            acc.add(item)
        }

        return loop(index + 1, acc)
    }

    return loop(0, mutableListOf())
}

Usage:

val evens = listOf(1, 2, 3, 4, 5, 6).filterTailrec { it % 2 == 0 }
println(evens) // [2, 4, 6]

Example: fold with tail recursion

A tail-recursive fold is a good general building block:

tailrec fun <T, R> foldTailrec(
    items: List<T>,
    index: Int = 0,
    acc: R,
    operation: (R, T) -> R
): R {
    return if (index == items.size) {
        acc
    } else {
        foldTailrec(
            items = items,
            index = index + 1,
            acc = operation(acc, items[index]),
            operation = operation
        )
    }
}

Usage:

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

val sum = foldTailrec(numbers, acc = 0) { acc, n -> acc + n }
val product = foldTailrec(numbers, acc = 1) { acc, n -> acc * n }

println(sum)     // 10
println(product) // 24

Avoid this: non-tail recursion

This is not tail-recursive:

fun sumBad(numbers: List<Int>): Int {
    return if (numbers.isEmpty()) {
        0
    } else {
        numbers.first() + sumBad(numbers.drop(1))
    }
}

The recursive call is not the last operation because Kotlin still has to add numbers.first() after sumBad(...) returns.

This is also inefficient because drop(1) creates new lists repeatedly.

Prefer index-based traversal for lists

For lists, prefer this:

tailrec fun sumGood(
    numbers: List<Int>,
    index: Int = 0,
    acc: Int = 0
): Int {
    return if (index == numbers.size) {
        acc
    } else {
        sumGood(numbers, index + 1, acc + numbers[index])
    }
}

Instead of this:

tailrec fun sumLessEfficient(
    numbers: List<Int>,
    acc: Int = 0
): Int {
    return if (numbers.isEmpty()) {
        acc
    } else {
        sumLessEfficient(numbers.drop(1), acc + numbers.first())
    }
}

Even though the second version is tail-recursive, drop(1) allocates a new list each time, which can make the algorithm much slower.

Important limitation

Kotlin’s tailrec optimization works only for direct self-recursion.

This can be optimized:

tailrec fun countDown(n: Int) {
    if (n > 0) countDown(n - 1)
}

But this cannot:

fun even(n: Int): Boolean {
    return n == 0 || odd(n - 1)
}

fun odd(n: Int): Boolean {
    return n != 0 && even(n - 1)
}

Mutual recursion is not optimized by tailrec.

Practical advice

For collection algorithms in Kotlin:

  • Use tailrec only when recursion makes the logic clearer.
  • Use an accumulator for partial results.
  • Prefer index-based traversal over drop, take, or subList loops.
  • For transformations, use a mutable accumulator internally and return it as List<T>.
  • Remember that Kotlin’s standard library functions like map, filter, fold, any, and find are usually the best choice in production code.

In most real Kotlin code, this:

val result = numbers.fold(0) { acc, n -> acc + n }

is preferable to writing your own recursive fold unless you are learning, implementing custom traversal logic, or working with recursive data structures.

How do I use lazy evaluation and avoid unnecessary computation in Kotlin?

In Kotlin, you can use lazy evaluation mainly with:

  1. lazy { ... } for lazily initialized properties
  2. Sequence for lazy collection-style pipelines
  3. Short-circuiting operators/functions like &&, ||, any, first, take
  4. Lambdas to defer expensive work until needed

1. Lazy property initialization with lazy

Use lazy when a value is expensive to create and may not always be needed.

val expensiveValue: String by lazy {
    println("Computing expensive value...")
    loadExpensiveData()
}

fun loadExpensiveData(): String {
    return "Result"
}

fun main() {
    println("Before access")

    // Computation happens here, on first access
    println(expensiveValue)

    // Reuses cached value, does not recompute
    println(expensiveValue)
}

Output:

Before access
Computing expensive value...
Result
Result

lazy computes the value only once by default.


2. Lazy collection processing with Sequence

Kotlin collections like List process intermediate operations eagerly:

val result = listOf(1, 2, 3, 4, 5)
    .map {
        println("map $it")
        it * 2
    }
    .filter {
        println("filter $it")
        it > 5
    }
    .first()

With a regular List, map creates a full intermediate list before filter runs.

Use asSequence() to process elements one at a time:

val result = listOf(1, 2, 3, 4, 5)
    .asSequence()
    .map {
        println("map $it")
        it * 2
    }
    .filter {
        println("filter $it")
        it > 5
    }
    .first()

println(result)

This stops as soon as first() finds a matching value.

For the example above, it processes only as much as needed:

map 1
filter 2
map 2
filter 4
map 3
filter 6
6

It does not process 4 or 5.


3. Prefer terminal operations that short-circuit

Some operations stop early when the answer is known.

Examples:

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

val hasEven = numbers.any { it % 2 == 0 }
val firstLarge = numbers.firstOrNull { it > 3 }
val firstThree = numbers.take(3)

These can avoid unnecessary work.

With sequences, this becomes especially useful:

val firstMatch = numbers
    .asSequence()
    .map { expensiveTransform(it) }
    .firstOrNull { it.isValid() }

This avoids transforming every element if a valid one appears early.


4. Use boolean short-circuiting

Kotlin’s && and || operators short-circuit.

if (user != null && user.isActive) {
    println("Active user")
}

user.isActive is checked only if user != null.

Another example:

if (cachedResult != null || computeFallback()) {
    println("We have a result")
}

computeFallback() runs only if cachedResult != null is false.


5. Defer expensive work with lambdas

If a function may not need a value, pass a lambda instead of the already-computed value.

Avoid this:

fun logDebug(message: String) {
    if (isDebugEnabled()) {
        println(message)
    }
}

logDebug("Result: ${expensiveComputation()}")

The expensive computation happens before logDebug is called.

Prefer this:

fun logDebug(message: () -> String) {
    if (isDebugEnabled()) {
        println(message())
    }
}

logDebug {
    "Result: ${expensiveComputation()}"
}

Now expensiveComputation() runs only if debug logging is enabled.


6. Use getOrPut for lazy map values

If you cache computed values in a map, use getOrPut:

val cache = mutableMapOf<String, Data>()

fun getData(key: String): Data {
    return cache.getOrPut(key) {
        loadData(key)
    }
}

loadData(key) only runs if the key is missing.


7. Build custom lazy sequences

You can create sequences using sequence { ... } and yield.

val generatedNumbers = sequence {
    println("Generating 1")
    yield(1)

    println("Generating 2")
    yield(2)

    println("Generating 3")
    yield(3)
}

val first = generatedNumbers.first()

println(first)

Only the first value is generated:

Generating 1
1

8. Be careful: sequences are not always faster

Use Sequence when:

  • You have a long chain of operations
  • You process large collections
  • You use short-circuiting terminal operations like first, any, take
  • You want to avoid intermediate collections

Regular collections may be better when:

  • The collection is small
  • The pipeline is simple
  • You need the final result as a list anyway
  • Performance overhead from sequences outweighs savings

Example where Sequence is useful:

val result = users
    .asSequence()
    .filter { it.isActive }
    .map { loadProfile(it) }
    .firstOrNull { it.hasPremiumAccess }

Example where a regular list is fine:

val names = users.map { it.name }

Common patterns

Lazy property

class ReportService {
    private val reportTemplate by lazy {
        loadTemplateFromDisk()
    }

    fun generateReport(): Report {
        return Report(reportTemplate)
    }
}

Lazy pipeline

val result = items
    .asSequence()
    .filter { shouldKeep(it) }
    .map { transform(it) }
    .take(10)
    .toList()

Lazy fallback

val value = cachedValue ?: computeExpensiveFallback()

computeExpensiveFallback() only runs if cachedValue is null.


Summary

Use:

  • by lazy { ... } for values initialized on first use
  • asSequence() for lazy collection pipelines
  • any, firstOrNull, take, etc. for short-circuiting
  • &&, ||, and ?: to avoid unnecessary calls
  • Lambdas like () -> String to defer expensive argument construction
  • getOrPut for lazy cache population

A good rule of thumb:

val result = data
    .asSequence()
    .filter { cheapCheck(it) }
    .map { expensiveTransform(it) }
    .firstOrNull { finalCheck(it) }

Filter early, transform late, and stop as soon as you have what you need.

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.