reduce and fold

reduce and fold are two of Kotlin’s most useful collection operations: they collapse an entire collection into a single value by repeatedly combining elements with an accumulator. Instead of writing a manual loop with a mutable variable, you describe the combining step once and Kotlin does the iteration for you. They show up everywhere in real code — summing prices, building strings, computing statistics, or turning a list into a map — so understanding exactly how they differ (and when to reach for their cousins reduceRight, foldRight, reduceOrNull, and the running* variants) will make your collection code shorter and safer.

Overview: How reduce and fold Work

Both functions walk a collection from left to right (by default), carrying an accumulator value from one step to the next. At each element, they call the lambda you provide with the current accumulator and the current element, and whatever the lambda returns becomes the new accumulator. After the last element, the final accumulator is the result. The entire difference between them comes down to one question: where does the initial accumulator come from?

reduce has no separate initial value — it uses the first element of the collection as the starting accumulator, then combines it with every element after that. This has two consequences the compiler and runtime both enforce: the accumulator type must be the same type as the elements (or a supertype of them), and the collection must not be empty, because there would be nothing to seed the accumulator with. Calling reduce on an empty collection compiles fine but throws an UnsupportedOperationException at runtime.

fold takes an explicit initial value as its first argument. Because you supply the seed yourself, the accumulator’s type R is completely independent of the element type T — you can fold a List<String> down into an Int, a Map, another list, or anything else. It also means fold works perfectly well on an empty collection: with nothing to iterate, it simply returns the initial value untouched.

Under the hood, both are inline functions in the standard library, so there is no lambda object allocated and no extra function-call overhead — the compiler splices your lambda’s body directly into a plain for loop over a local var accumulator, exactly like the loop you would have written by hand. That is also why the accumulator variable itself never leaks out of the function; you only ever see it as the acc parameter inside your lambda and as the final returned value.

Function Needs initial value? Works on empty collection? Direction Result type
reduce No (uses first element) No — throws Left to right Same as element type (or supertype)
reduceRight No (uses last element) No — throws Right to left Same as element type (or supertype)
reduceOrNull No Yes — returns null Left to right Element type, or null
fold Yes Yes — returns initial value Left to right Any type R
foldRight Yes Yes — returns initial value Right to left Any type R
runningFold Yes Yes — list with just the initial value Left to right List<R> of every intermediate step
runningReduce No Yes — empty list Left to right List<T> of every intermediate step

Syntax

// reduce: no initial value, accumulator type S must be a supertype of element type T
inline fun <S, T : S> Iterable<T>.reduce(
    operation: (acc: S, T) -> S
): S

// fold: explicit initial value, accumulator type R is unrelated to element type T
inline fun <T, R> Iterable<T>.fold(
    initial: R,
    operation: (acc: R, T) -> R
): R
  • operation — the lambda called once per element; its first parameter is the running accumulator, its second is the current element, and its return value becomes the next accumulator.
  • initial (fold only) — the starting accumulator value, supplied by you before iteration begins.
  • S / R — the accumulator’s type. For reduce it is constrained to the element type or one of its supertypes; for fold it can be any type at all.
  • The receiver can be any Iterable<T> — lists, sets, and sequences all support both functions.

Examples

Example 1: Summing numbers with reduce

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val sum = numbers.reduce { acc, n -> acc + n }
    println(sum)
}

Output:

15

The accumulator starts as the first element, 1, then each remaining element is added in turn: 1+2=3, 3+3=6, 6+4=10, 10+5=15. No initial value was needed because the list was already non-empty and the result type (Int) matches the element type.

Example 2: fold with an explicit starting value

fun main() {
    val scores = listOf(10, 20, 15, 25)
    val total = scores.fold(5) { acc, score -> acc + score }
    println("Total score: $total")
}

Output:

Total score: 75

Here the accumulator starts at 5 (think of it as a starting bonus) instead of the first element, so the final result includes that bonus: 5+10+20+15+25=75. Unlike reduce, this would also work correctly if scores were empty — the result would simply be 5.

Example 3: fold with a type-changing accumulator

fun main() {
    val words = listOf("a", "b", "a", "c", "b", "a")
    val counts = words.fold(mutableMapOf<String, Int>()) { acc, word ->
        acc[word] = acc.getOrElse(word) { 0 } + 1
        acc
    }
    println(counts)
}

Output:

{a=3, b=2, c=1}

This is where fold shows its real power: the collection holds Strings, but the accumulator is a MutableMap<String, Int> — a completely different type. reduce could never do this, because its accumulator type is tied to the element type. Each step looks up the word’s current count with getOrElse (defaulting to 0 if it hasn’t been seen), increments it, and returns the same map reference as the new accumulator.

How it works step by step

Tracing Example 1’s reduce call element by element:

Step Element Accumulator before Accumulator after
seed 1 (first element)
1 2 1 3
2 3 3 6
3 4 6 10
4 5 10 15

Notice reduce never calls the lambda for the first element — it only seeds the accumulator with it. Tracing Example 2’s fold call, the lambda runs once per element including the first, because the seed came from initial instead: accumulator goes 5 → 15 → 35 → 50 → 75 as each of 10, 20, 15, 25 is added.

Common Mistakes

Mistake 1: Calling reduce on a collection that might be empty

fun main() {
    val numbers = emptyList<Int>()
    val sum = numbers.reduce { acc, n -> acc + n }
    println(sum)
}

Output:

Throws java.lang.UnsupportedOperationException: Empty collection can't be reduced. (println never runs; the program crashes first)

This compiles without any warning because the compiler cannot know at compile time whether a List will be empty at runtime. The fix is to use fold with a sensible initial value, or reduceOrNull if you specifically want reduce‘s seed-from-first-element behavior but need a graceful null instead of a crash.

fun main() {
    val numbers = emptyList<Int>()
    val sum = numbers.fold(0) { acc, n -> acc + n }
    println(sum)
}

Output:

0

Mistake 2: Trying to use reduce when the accumulator type differs from the element type

val numbers = listOf(1, 2, 3)
val result = numbers.reduce { acc: List<Int>, n -> acc + n }
// Type mismatch: reduce requires the accumulator's type to be a
// supertype of the element type (Int here), so List<Int> is rejected.

Beginners often reach for reduce when they actually want to build up a different kind of value (a list, a map, a string) from the elements. reduce simply cannot express that, because its accumulator type is constrained to the element type by its own generic signature. fold has no such constraint:

fun main() {
    val numbers = listOf(1, 2, 3)
    val result = numbers.fold(emptyList<Int>()) { acc, n -> acc + n }
    println(result)
}

Output:

[1, 2, 3]

Mistake 3: Assuming reduce and reduceRight always agree

fun main() {
    val numbers = listOf(10, 2, 3)
    val leftToRight = numbers.reduce { acc, n -> acc - n }
    val rightToLeft = numbers.reduceRight { n, acc -> n - acc }
    println("reduce: $leftToRight")
    println("reduceRight: $rightToLeft")
}

Output:

reduce: 5
reduceRight: 11

For a commutative, associative operation like addition, direction does not matter. But subtraction is neither, so reduce (left to right: ((10-2)-3)=5) and reduceRight (right to left: 10-(2-3)=11) give genuinely different answers on the same input. Always check whether your combining operation is order-sensitive before assuming the two are interchangeable, and note that reduceRight‘s lambda receives the element first and the accumulator second — the opposite parameter order from reduce.

Best Practices

  • Default to fold unless you specifically know the collection is non-empty and the result type matches the element type — it is strictly more flexible and never throws for empty input.
  • Reach for reduceOrNull when you want reduce‘s seed-from-first-element behavior but need to handle an empty collection without a crash or a try/catch.
  • Use fold whenever the accumulator’s type differs from the elements’ type, such as building a Map, a String, or a new list.
  • Prefer runningFold or runningReduce when you need every intermediate accumulator value (for example, a running total for a chart), not just the final result.
  • Watch operation order for non-commutative operations (subtraction, division, string concatenation in a specific order) — pick fold/reduce versus foldRight/reduceRight deliberately.
  • Keep the operation lambda pure — return the new accumulator instead of mutating external variables, so the fold stays easy to reason about and test.
  • Name the lambda parameters explicitly (acc, item) rather than relying on Kotlin’s implicit it, since fold/reduce lambdas always take two parameters and it is not available for multi-parameter lambdas.

Practice Exercises

  • Given listOf(3, 7, 2, 9, 4), use reduce (not maxOrNull()) to find the largest value. Expected output: 9.
  • Given listOf("Kotlin", "is", "concise"), use fold with an initial empty String to join the words into a single sentence separated by spaces. Expected output: Kotlin is concise.
  • Given listOf(1, 2, 3, 4), use fold with an initial value of 1 to compute the product of all elements. Expected output: 24.

Summary

  • reduce seeds its accumulator from the first element, requires a non-empty collection, and keeps the accumulator type tied to the element type.
  • fold takes an explicit initial value, works on empty collections, and lets the accumulator be any type — independent of the element type.
  • Both compile to a simple left-to-right loop with no extra allocation, since they are inline functions.
  • reduceRight and foldRight walk right to left, which produces different results from their left-to-right counterparts for non-commutative operations.
  • reduceOrNull avoids the empty-collection crash of reduce by returning null instead.
  • runningFold and runningReduce return every intermediate accumulator value as a list, not just the final one.
  • When in doubt, default to fold — it is a strict superset of what reduce can do.