🚗 · Aggregators

6 min read · Updated on by

Read on for an introduction to the most important functions for aggregation: fold, reduce, average, count, sum, min, max, and their variants. A quick note on converting collections to strings using joinToString.

Aggregators

fold


//sampleStart
inline fun <T, R> Iterable<T>.fold(
    initial: R,
    operation: (R, T) -> R
): R
//sampleEnd

The most fundamental aggregation operation is fold. The way it works is simple, but iit doesn’t lend itself well to word-based descriptions, so I’ll demonstrate it instead, and then show you one way it could be implemented.


//sampleStart
fun main() {
    // 1. First sets result = 0, element = 1 and calculates 0 + 1 = 1. 
    //    This becomes the value for result in the next iteration.
    // 
    // 2. result = 1, element = 2 -> 1 + 2 = 3
    // 3. result = 3, element = 3 -> 3 + 3 = 6
    listOf(1, 2, 3).fold(0) { result, element -> result + element }.also(::println) // 6

    listOf("a", "b", "c").fold("Letters:") { letters, letter -> 
        "$letters ${letter}_$letter"
    }.also(::println) // "Letters: a_a b_b c_c"

    listOf(1, 2, 3).fold("") { result, number -> "$result$number" }.also(::println) // "123"
}
//sampleEnd

//sampleStart
inline fun <T, R> Iterable<T>.fold(
    initial: R,
    operation: (R, T) -> R
): R {
    var result = initial
    for(element in this) {
        result = operation(result, element)
    }
    return result
}
//sampleEnd

As always, there are many variations of fold. One of them has a seemingly different name, reduce:


//sampleStart
inline fun <S, T : S> Iterable<T>.reduce(operation: (S, T) -> S): S
//sampleEnd

The difference between reduce and fold is that there is no initial element, and the operation accepts and produces types that are related by inheritance. In essence, reduce is fold when the operation produces a type that is compatible with the argument and no initial value is needed.

The first example from above could therefore be rewritten using reduce:


//sampleStart
fun main() {
    // 1. Takes 5 and 3, and applies Int::plus to them to get 8
    // 2. Takes 8 (from the previous step) and 12 (the next element), 
    //    and applies Int::plus to them to get 20
    listOf(5, 3, 12).fold(0) { result, element -> result + element }.also(::println) // 20

   
    listOf(1, 2, 3).reduce(Int::plus).also(::println) // 6
}
//sampleEnd

It is key to remember that, since there is no initial value, reduce cannot be used with empty collections, and will throw an UnsupportedOperationException in such a situation. However, there is also a reduceOrNull variant that returns null if run on an empty collection.

Both fold and reduce have *Right variants, where the elements in the successive iterations are taken from the end instead of the beginning. Be careful! The order of the arguments is switched relative to the regular variants.


//sampleStart
fun main() {
    listOf("a", "b", "c").foldRight("Letters:") { letter, letters -> 
        "$letters ${letter}_$letter"
    }.also(::println) // "Letters: c_c b_b a_a"
}
//sampleEnd

Another are the *Indexed variants, which, as you’ve come to expect, also send the element index into the operation.


//sampleStart
inline fun <T, R> Iterable<T>.runningFoldIndexed(
    initial: R, 
    operation: (Int, R, T) -> R
): List<R>
inline fun <S, T : S> Iterable<T>.reduceIndexed(
    operation: (Int, S, T) -> S
): S
//sampleEnd

There are also running* variants for foldfoldIndexedreduce and reduceIndexed (i.e. runningFold etc.). Instead of returning the final value, they instead return a list which includes all the intermediate values from the successive iterations:


//sampleStart
fun main() {
    // 1. First sets result = 0, element = 1 and calculates 0 + 1 = 1. 
    //    This becomes the value for result in the next iteration.
    // 
    // 2. result = 1, element = 2 -> 1 + 2 = 3
    // 3. result = 3, element = 3 -> 3 + 3 = 6
    listOf(1, 2, 3).fold(0) { result, element -> result + element }.also(::println) // 6

    listOf(1, 2, 3).runningFold(0) { result, element -> result + element }.also(::println) //     listOf(0, 1, 3, 6)

    listOf(1, 2, 3).runningReduce(Int::plus).also(::println) // listOf(1, 3, 6)
}
//sampleEnd

An alias for runningFold and runningFoldIndexed are scan and scanIndexed.

Statistics

average

Defined for Iterable<Byte>Iterable<Double>Iterable<Float>Iterable<Int>Iterable<Long> and Iterable<Short>, returns the average of the list.

count


//sampleStart
fun <T> Iterable<T>.count(): Int
fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int
//sampleEnd

Returns the number of elements in the list. A more interesting variant accepts a predicate, and returns the number of elements satisfying that predicate.

sum, sumOf

The sum method is defined for Iterable<Byte>Iterable<Double>Iterable<Float>Iterable<Int>Iterable<Long> and Iterable<Short>, returns the sum of the elements of the list.

The sumOf method is defined for any Iterable<T>, accepts a lambda that transforms a T into a numeric type, and returns the sum of those numeric values. Permissible numeric types are DoubleIntLongUIntULongBigDecimal and BigInteger.

Both return 0 (of the appropriate type) if the collection is empty.

Example


//sampleStart
interface Person

interface EmployedPerson : Person {
    val salary: Int
}

typealias Household = Set<Person>

val Household.income get() = 
    filterIsInstance<EmployedPerson>().sumOf { it.salary }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the composer in the code's symphony,
        With delegates and lambdas, pure harmony.
        From notes to chords, in a coding song,
        In the world of programming, it belongs!
    """.trimIndent()
    println(poem)
}

min, minBy, minOf


//sampleStart
fun <T : Comparable<T>> Iterable<T>.min(): T
inline fun <T, R : Comparable<R>> Iterable<T>.minBy(
    selector: (T) -> R
): T
inline fun <T, R : Comparable<R>> Iterable<T>.minOf(
    selector: (T) -> R
): R
//sampleEnd

Various functions which calculate the minimum of a collection of comparable elements. The difference between minBy and minOf is what gets returned — minBy returns the element of the receiver for which the selector yields the smallest value, while minOf returns the actual value.

Example


//sampleStart
interface Person

interface EmployedPerson : Person {
    val salary: Int
}

typealias Household = Set<Person>

val Household.lowestIncome get(): Int = 
    filterIsInstance<EmployedPerson>().minOf { it.salary } 
    
val Household.personWithLowestIncome get(): EmployedPerson = 
    filterIsInstance<EmployedPerson>().minBy { it.salary }
//sampleEnd
fun main() {
    val poem = """
        When you're sailing in the sea of code,
        Kotlin's syntax is the compass, the road.
        With waves and currents, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

All of the above throw NoSuchElementException if used on an empty list. Alternatively, there are *orNull variants which return null for empty lists.

There are also *With variants (e.g. minWithminOfWithminOfWithOrNull) which allow you to specify a Comparator for collections of elements which are not comparable.

Example


//sampleStart
interface Person

interface EmployedPerson : Person {
    val salary: Int
}

typealias Household = Set<Person>
    
val employedPersonComparator: Comparator<EmployedPerson> = compareBy { it.salary }


val Household.personWithLowestIncome get(): EmployedPerson =
    filterIsInstance<EmployedPerson>().minWith(employedPersonComparator)
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's tower,
        With sealed classes, it builds the power.
        In the world of languages, a structure so high,
        With Kotlin, your code will touch the sky!
    """.trimIndent()
    println(poem)
}

max, maxBy, maxOf

Exactly the same as their min counterparts, except they operate with largest values instead of smallest ones.

Conversion to String


//sampleStart
fun <T> Iterable<T>.joinToString(
    separator: CharSequence = ", ",
    prefix: CharSequence = "",
    postfix: CharSequence = "",
    limit: Int = -1,
    truncated: CharSequence = "...",
    transform: ((T) -> CharSequence)? = null
): String
//sampleEnd

It is often useful to join a whole collection into string, with the ability to specify separators, a pre-/postfix, etc. This is exactly what the joinToString function is for.

The meaning of its various parameters is best demonstrated by example:


//sampleStart
fun main() {
    listOf(1, 2, 3, 4).joinToString(
        separator = " + ", 
        prefix = "Here's an example of summing numbers: ",
        postfix = " = 14",
        transform = { (it + 1).toString() }
    ).also(::println) // Here's an example of summing numbers: 2 + 3 + 4 + 5 = 14


    ('a'..'z').joinToString(
	prefix = "The alphabet starts with ",
        limit = 4,
        truncated = "... and so on"
    ).also(::println) // The alphabet starts with a, b, c, d, ... and so on
}
//sampleEnd
The Kotlin Primer