1. Overview
In this tutorial, we’re going to explore the differences between the fold() and reduce() methods in Kotlin.
Despite the fact that both functions traverse a collection and apply a given operation, they’re quite different.
2. reduce()
The reduce() method transforms a given collection into a single result. It takes a lambda function operator to combine a pair of elements into a so-called accumulated value.
It then traverses the collection from left to right and stepwise combines the accumulated value with the next element.
To see this in action, let’s use reduce to calculate the sum of a list of numbers:
val numbers: List<Int> = listOf(1, 2, 3)
val sum: Int = numbers.reduce { acc, next -> acc + next }
assertEquals(6, sum)
What would happen in the case of an empty list? Actually, there’s no right value to return, so reduce() throws a RuntimeException:
val emptyList = listOf<Int>()
assertThrows<RuntimeException> { emptyList.reduce { acc, next -> acc + next } }
One could argue that returning 0 would be a valid result in this case. As we’ll see, fold() gives more flexibility on that.
To understand another characteristic, let’s have a look at the signature of reduce():
inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S
The function defines a generic type S and a subtype T of S. Finally, reduce() returns one value of type S.
Let’s assume in the preceding example that the sum could exceed the range of Int. Because of that, we’ll change the result type to Long:
// doesn't compile
val sum: Long = numbers.reduce<Long, Int> { acc, next -> acc.toLong() + next.toLong() }
Obviously, it doesn’t compile because Long is not a supertype of Int. To fix the compile error, we can instead change the Long type to Number, because Number is a supertype of Int.
However, it doesn’t solve the general problem of changing the result type.
3. fold()
So, let’s see if we can address these issues by implementing the preceding sum example using fold():
val sum: Int = numbers.fold(0){ acc, next -> acc + next }
assertEquals(6, sum)
Here, we’ve provided an initial value. *In contrast to reduce(), if the collection is empty, the initial value will be returned.*
To dig deeper, let’s have a look at the signature of fold():
inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R
In contrast to reduce(), it specifies two arbitrary generic types T and R.
So, we can change the result type to Long:
val sum: Long = numbers.fold(0L){ acc, next -> acc + next.toLong() }
assertEquals(6L, sum)
In general, the ability to change the result type is a very powerful tool. For example, if we use the right result types, we can easily split the collection into even and odd numbers:
val (even, odd) = numbers.fold(Pair(mutableListOf<Int>(), mutableListOf<Int>())) { eoPair, number ->
eoPair.apply {
when (number % 2) {
0 -> first += number
else -> second += number
}
}
}
assertEquals(listOf(2), even)
assertEquals(listOf(1, 3), odd)
4. Variations of fold and reduce
We’ve seen basic variants of both functions; however, the Kotlin standard library provides some variations of them, too.
If we need to traverse the collections in converse order from right to left, we can use foldRight():
val reversed = numbers.foldRight(listOf<Int>()) { next, acc -> acc + next }
assertEquals(listOf(3,2,1), reversed)
We should note that *when we use foldRight(), the order of the lambda’s parameters is reversed: foldRight(…) { next , acc -> …}*
Similarly, we can use reduceRight().
Additionally, we might want to access the index of each element in the collection, too:
val reversedIndexes = numbers.foldRightIndexed(listOf<Int>()) { i, _, acc -> acc + i }
assertEquals(listOf(2,1,0), reversedIndexes)
Similarly, we can use reduceRightIndexed().
Moreover, foldIndexed() and reduceIndexed() provide access to indices with the basic left to right order.
5. Conclusion
So, we’ve seen the differences between fold() and reduce().
On the one hand, if we operate only on a non-empty collection and combine all elements into a single result of the same type, then reduce() is a good choice. On the other hand, if we want to provide an initial value or change the result type, then fold() gives us the flexibility to do it.
To dive deeper, we recommend taking a look at Kotlin’s collection transformations.
And, as always, the code examples can be found over on GitHub.