Avatar ·

В чем разница между fold и reduce в Kotlin и когда что использовать?

Всем привет! Я только начал изучать Kotlin и перешел с Java. Читаю документацию по коллекциям и наткнулся на две функции: fold и reduce. По описанию они обе делают что-то похожее — проходят по элементам списка и накапливают результат. Но я никак не могу понять, в чем между ними принципиальная разница. Вот пример, который я написал: ```kotlin val numbers = listOf(1, 2, 3, 4) val sumWithFold = numbers.fold(0) { acc, i -> acc + i } val sumWithReduce = numbers.reduce { acc, i -> acc + i } println("Fold: $sumWithFold") // 10 println("Reduce: $sumWithReduce") // 10 ``` Оба дают одинаковый результат. Но я заметил, что fold принимает начальное значение (initial value), а reduce — нет. Когда я попробовал использовать reduce на пустом списке, у меня вылетело исключение. А fold сработал нормально и вернул начальное значение. Также я видел, что в некоторых примерах используют fold вместо reduce, но с начальным значением, равным первому элементу. Зачем так делать, если есть reduce? Можете объяснить на пальцах: когда нужно использовать fold, а когда reduce? И есть ли какие-то подводные камни? Буду благодарен за примеры из реальной жизни. <<>>

Ключевое отличие: начальное значение и работа с пустыми коллекциями

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

1. Тип результата

reduce всегда возвращает тот же тип, что и элементы коллекции, потому что аккумулятор изначально равен первому элементу. fold может возвращать любой тип, который вы укажете через начальное значение.

val numbers = listOf(1, 2, 3)

// Reduce: тип результата = Int (как у элементов)
val sum = numbers.reduce { acc, i -> acc + i } // Int

// Fold: тип результата = String (не как у элементов!)
val asString = numbers.fold("") { acc, i -> "$acc$i" } // "123"

Это очень полезно, когда нужно преобразовать коллекцию в другой тип данных.

2. Работа с пустыми коллекциями

reduce выбросит UnsupportedOperationException на пустой коллекции, потому что не из чего взять первый элемент. fold просто вернёт начальное значение.

val emptyList = listOf()

// Ошибка!
// emptyList.reduce { acc, i -> acc + i }

// Безопасно: вернёт 0
val safeSum = emptyList.fold(0) { acc, i -> acc + i } // 0

Если вы не уверены, что коллекция не пуста, используйте fold или проверяйте ifEmpty перед reduce.

3. Производительность и читаемость

Поскольку reduce не создаёт дополнительный объект для начального значения, он может быть чуть быстрее на больших коллекциях. Но разница обычно незначительна. Главное — читаемость кода.

  • Используйте reduce, когда:
    • Коллекция гарантированно не пуста.
    • Результат должен быть того же типа, что и элементы.
    • Логика аккумуляции естественно начинается с первого элемента (например, поиск максимума).
  • Используйте fold, когда:
    • Коллекция может быть пустой.
    • Тип результата отличается от типа элементов.
    • Нужно задать явное начальное значение (например, для конкатенации строк с префиксом).

Реальные примеры

data class Product(val name: String, val price: Double)

val products = listOf(
    Product("Phone", 500.0),
    Product("Laptop", 1200.0),
    Product("Mouse", 25.0)
)

// Reduce: сумма цен (тип результата = Double)
val totalPrice = products.reduce { acc, product -> 
    Product("total", acc.price + product.price) 
}.price

// Fold: строка с названиями через запятую (тип результата = String)
val names = products.fold("Products: ") { acc, product -> 
    "$acc${product.name}, " 
}
println(names) // "Products: Phone, Laptop, Mouse, "

Подводные камни

  • Порядок выполнения: обе функции обрабатывают элементы слева направо. Для обратного порядка есть foldRight и reduceRight.
  • Изменяемые аккумуляторы: старайтесь не мутировать аккумулятор внутри лямбды. Используйте неизменяемые объекты или run блок для локальных мутаций.
  • Производительность: на больших коллекциях (миллионы элементов) разница между fold и reduce может быть заметна из-за создания лишних объектов. В таких случаях рассмотрите foldIndexed или цикл for.

Итог

Если кратко: fold — это универсальный инструмент с явным начальным значением, reduce — частный случай для непустых коллекций с тем же типом результата. Начинайте с fold, если не уверены — он безопаснее. Переходите на reduce, когда хотите подчеркнуть, что коллекция точно не пуста и тип результата совпадает с типом элементов.

<<>>

Дополнение: foldIndexed и reduceIndexed

В Kotlin есть также версии с индексом: foldIndexed и reduceIndexed. Они передают в лямбду не только аккумулятор и элемент, но и его индекс. Это удобно, когда нужно учитывать позицию элемента.

val numbers = listOf(10, 20, 30)

// foldIndexed: добавляем индекс к каждому элементу
val withIndex = numbers.foldIndexed(mutableListOf()) { index, acc, value ->
    acc.add("[$index] $value")
    acc
}
println(withIndex) // [[0] 10, [1] 20, [2] 30]

// reduceIndexed: проверяем, что все элементы положительные
val allPositive = numbers.reduceIndexed { index, acc, value ->
    if (value <= 0) throw IllegalArgumentException("Negative at $index")
    acc + value
}

Когда это полезно?

  • При работе с матрицами или таблицами, где важна позиция.
  • Для создания отчётов с номерами строк.
  • Для валидации данных с указанием индекса ошибки.

Совет: не злоупотребляйте индексными версиями — они усложняют код. Используйте их только когда индекс действительно нужен для логики аккумуляции.