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 fold
, foldIndexed
, reduce
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 Double
, Int
, Long
, UInt
, ULong
, BigDecimal
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. minWith
, minOfWith
, minOfWithOrNull
) 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