Операции объединения

Коллекции Kotlin содержат функции для часто используемых операций объединения (ориг. aggregate operations), которые возвращают одно значение на основе содержимого коллекции. Большинство из них хорошо известны и работают так же, как и на других языках:

  • minOrNull() и maxOrNull() возвращают наименьший и наибольший элемент коллекции соответственно. Если коллекция пуста, то вернётся null.
  • average() возвращает среднее значение элементов в коллекции чисел.
  • sum() возвращает сумму элементов в коллекции чисел.
  • count() возвращает количество элементов в коллекции.
fun main() {
    val numbers = listOf(6, 42, 10, 4)

    println("Count: ${numbers.count()}") // Count: 4
    println("Max: ${numbers.maxOrNull()}") // Max: 42
    println("Min: ${numbers.minOrNull()}") // Min: 4
    println("Average: ${numbers.average()}") // Average: 15.5
    println("Sum: ${numbers.sum()}") // Sum: 62
}

Также существуют функции для получения самых маленьких и самых больших элементов в коллекции с помощью определённой функции-селектора или пользовательского Comparator:

  • maxByOrNull() и minByOrNull() принимают функцию-селектор и возвращают элемент, для которого эта функция возвращает наибольшее или наименьшее значение.
  • maxWithOrNull() и minWithOrNull() принимают объект Comparator и возвращают наибольший или наименьший элемент, соответствующий этому Comparator.

Если коллекция пуста, то эти функции вернут null. У функций maxByOrNull() и minByOrNull() есть аналоги: maxOf() и minOf(), которые работают аналогично, но бросают исключение NoSuchElementException, если коллекция пуста.

fun main() {
    val numbers = listOf(5, 42, 10, 4)
    val min3Remainder = numbers.minByOrNull { it % 3 }
    println(min3Remainder) // 42

    val strings = listOf("one", "two", "three", "four")
    val longestString = strings.maxWithOrNull(compareBy { it.length })
    println(longestString) // three
}

Помимо обычного sum(), существует расширенная функция sumOf(). Она принимает функцию-селектор, которая применяет заданную операцию к каждому элементу коллекции, и возвращает сумму всех элементов с учётом этих изменений. Функция-селектор может возвращать различные числовые типы: Int, Long, Double, UInt, и ULong (а также BigInteger и BigDecimal из JVM).

fun main() {
    val numbers = listOf(5, 42, 10, 4)
    println(numbers.sumOf { it * 2 }) // 122
    println(numbers.sumOf { it.toDouble() / 2 }) // 30.5
}

Fold и reduce

Для более специфичных случаев существуют функции reduce() и fold(), которые последовательно применяют предоставленную операцию к элементам коллекции и возвращают накопленный результат. Операция принимает два аргумента: ранее накопленное значение и элемент коллекции.

Разница между этими двумя функциями состоит в том, что fold() принимает начальное значение и использует его как накопленное значение на первом шаге, тогда как reduce() на первом шаге в качестве аргументов операции использует первый и второй элементы.

fun main() {
    val numbers = listOf(5, 2, 10, 4)

    val simpleSum = numbers.reduce { sum, element -> sum + element }
    println(simpleSum) // 21
    val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 }
    println(sumDoubled) // 42

    // некорректно: первый элемент не будет удвоен
    // val sumDoubledReduce = numbers.reduce { sum, element -> sum + element * 2 }
    // println(sumDoubledReduce)
}

В примере выше показана разница: fold() используется для вычисления суммы удвоенных элементов. Если вы передадите ту же функцию в reduce(), она вернёт другой результат, поскольку он использует первый и второй элементы списка в качестве аргументов на первом шаге, в связи с чем первый элемент не будет удвоен.

Существуют функции reduceRight() и foldRight(), которые работают аналогично fold() и reduce(), но в обратном порядке: начинают с последнего элемента, а затем переходят к предыдущему. Обратите внимание, что при использовании foldRight() или reduceRight() аргументы операции меняются местами: сначала идёт элемент, а затем накопленное значение.

fun main() {
    val numbers = listOf(5, 2, 10, 4)
    val sumDoubledRight = numbers.foldRight(0) { element, sum -> sum + element * 2 }
    println(sumDoubledRight) // 42
}

Также вы можете использовать функции reduceIndexed() и foldIndexed(), которые дополнительно дают доступ к индексам элементов в качестве первого аргумента операции. Либо функции reduceRightIndexed() и foldRightIndexed(), которые работают аналогично, но в обратном порядке.


fun main() {
    val numbers = listOf(5, 2, 10, 4)
    val sumEven = numbers.foldIndexed(0) { idx, sum, element -> if (idx % 2 == 0) sum + element else sum }
    println(sumEven) // 15

    val sumEvenRight = numbers.foldRightIndexed(0) { idx, element, sum -> if (idx % 2 == 0) sum + element else sum }
    println(sumEvenRight) // 15
}

Все reduce-операции бросают исключение, если коллекция пуста. Чтобы вместо исключения возвращался null, используйте их аналоги с постфиксом *OrNull():

Для случаев, когда вы хотите сохранить промежуточное накопленное значение, существуют функции runningFold() (или её синоним scan()) и runningReduce().

fun main() {
    val numbers = listOf(0, 1, 2, 3, 4, 5)
    val runningReduceSum = numbers.runningReduce { sum, item -> sum + item }
    val runningFoldSum = numbers.runningFold(10) { sum, item -> sum + item }

    val transform = { index: Int, element: Int -> "N = ${index + 1}: $element" }
    println(runningReduceSum.mapIndexed(transform).joinToString("\n", "Sum of first N elements with runningReduce:\n"))
    println(runningFoldSum.mapIndexed(transform).joinToString("\n", "Sum of first N elements with runningFold:\n"))

    // В логе будет:
    // Sum of first N elements with runningReduce:
    // N = 1: 0
    // N = 2: 1
    // N = 3: 3
    // N = 4: 6
    // N = 5: 10
    // N = 6: 15
    // Sum of first N elements with runningFold:
    // N = 1: 10
    // N = 2: 10
    // N = 3: 11
    // N = 4: 13
    // N = 5: 16
    // N = 6: 20
    // N = 7: 25
}

Если вам нужен индекс в качестве параметра операции, используйте runningFoldIndexed() или runningReduceIndexed().