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. Forreduceit is constrained to the element type or one of its supertypes; forfoldit 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
foldunless 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
reduceOrNullwhen you wantreduce‘s seed-from-first-element behavior but need to handle an empty collection without a crash or atry/catch. - Use
foldwhenever the accumulator’s type differs from the elements’ type, such as building aMap, aString, or a new list. - Prefer
runningFoldorrunningReducewhen 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/reduceversusfoldRight/reduceRightdeliberately. - 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 implicitit, sincefold/reducelambdas always take two parameters anditis not available for multi-parameter lambdas.
Practice Exercises
- Given
listOf(3, 7, 2, 9, 4), usereduce(notmaxOrNull()) to find the largest value. Expected output:9. - Given
listOf("Kotlin", "is", "concise"), usefoldwith an initial emptyStringto join the words into a single sentence separated by spaces. Expected output:Kotlin is concise. - Given
listOf(1, 2, 3, 4), usefoldwith an initial value of1to compute the product of all elements. Expected output:24.
Summary
reduceseeds its accumulator from the first element, requires a non-empty collection, and keeps the accumulator type tied to the element type.foldtakes 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.
reduceRightandfoldRightwalk right to left, which produces different results from their left-to-right counterparts for non-commutative operations.reduceOrNullavoids the empty-collection crash ofreduceby returningnullinstead.runningFoldandrunningReducereturn every intermediate accumulator value as a list, not just the final one.- When in doubt, default to
fold— it is a strict superset of whatreducecan do.
