Fold vs Reduce в Kotlin: когда что использовать? Полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Fold или Reduce в Kotlin: вечный вопрос новичков

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

В этой статье мы разберем разницу между fold и reduce в Kotlin, посмотрим на живые примеры из реальных проектов и навсегда запомним, когда какую функцию использовать. Поехали!

Что такое fold и reduce? Базовое понимание

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

Reduce: минималистичный подход

Функция reduce использует первый элемент коллекции как начальное значение аккумулятора. Это значит, что она просто не может работать с пустой коллекцией — вылетит UnsupportedOperationException.

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.reduce { acc, i -> acc + i }
println(sum) // 15

Здесь acc изначально равен 1 (первый элемент), потом к нему добавляется 2, потом 3 и так далее. Всё просто и элегантно, но есть нюанс.

Fold: контроль над началом

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

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.fold(0) { acc, i -> acc + i }
println(sum) // 15

val emptyList = listOf<Int>()
val emptySum = emptyList.fold(0) { acc, i -> acc + i }
println(emptySum) // 0, никакой ошибки!

Видите разницу? fold возвращает начальное значение, если коллекция пуста. reduce в такой ситуации упадёт.

Ключевые различия между fold и reduce в Kotlin

Давайте систематизируем знания. Вот таблица, которая поможет вам быстро сориентироваться:

  • Начальное значение: В fold вы задаёте его сами. В reduce — это первый элемент коллекции.
  • Тип результата: В fold тип результата может отличаться от типа элементов (например, складываете Int, а получаете String). В reduce тип результата всегда совпадает с типом элементов.
  • Пустая коллекция: fold обрабатывает её нормально (возвращает начальное значение). reduce выбрасывает исключение.
  • Производительность: Обе функции работают за O(n). Разница в производительности незаметна.

Теперь давайте посмотрим на это в коде.

Пример 1: Разные типы аккумулятора и элементов

Допустим, у нас есть список чисел, и мы хотим получить строку — их сумму в читаемом виде. fold справляется на ура, потому что тип аккумулятора (String) отличается от типа элементов (Int).

val numbers = listOf(1, 2, 3)
val result = numbers.fold("Сумма: ") { acc, i -> "$acc$i " }
println(result) // "Сумма: 1 2 3 "

Попробуйте сделать то же самое с reduce — компилятор выдаст ошибку, потому что reduce требует, чтобы тип аккумулятора совпадал с типом элементов.

Пример 2: Подсчёт суммы с учётом начального значения

Частая задача: подсчитать общую сумму заказов, но у нас уже есть начальный баланс на счёте.

val orders = listOf(100, 250, 75)
val initialBalance = 500
val totalBalance = orders.fold(initialBalance) { balance, order -> balance + order }
println(totalBalance) // 925

С reduce так не получится — он начнёт с первого заказа (100), проигнорировав ваш initialBalance.

Когда использовать fold, а когда reduce?

Вот практические рекомендации, которые я вывел за годы работы с Kotlin:

Используйте reduce, если:

  • Вы абсолютно уверены, что коллекция не пуста.
  • Тип результата совпадает с типом элементов.
  • Вам не нужно начальное значение (первый элемент — отличная отправная точка).
  • Вы хотите написать чуть более лаконичный код.

Пример: подсчёт произведения всех чисел в непустом списке.

val numbers = listOf(2, 3, 4)
val product = numbers.reduce { acc, i -> acc * i }
println(product) // 24

Используйте fold, если:

  • Коллекция может быть пустой (это самый частый случай в реальном коде).
  • Вам нужно начальное значение, отличное от первого элемента.
  • Тип результата отличается от типа элементов.
  • Вы пишете библиотечный код, где поведение должно быть предсказуемым.

Пример: подсчёт количества символов во всех строках списка.

val words = listOf("Kotlin", "Java", "Python")
val totalChars = words.fold(0) { acc, word -> acc + word.length }
println(totalChars) // 16

Типичные ошибки и подводные камни

Даже опытные разработчики иногда путают эти функции. Вот что нужно запомнить:

Ошибка №1: Reduce на пустой коллекции

val emptyList = listOf<Int>()
// emptyList.reduce { a, b -> a + b } // UnsupportedOperationException!

Всегда проверяйте коллекцию на пустоту перед вызовом reduce, либо используйте reduceOrNull (Kotlin 1.4+)

Ошибка №2: Забыли про начальное значение в fold

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

Ошибка №3: Использование fold, когда нужен reduce

Если коллекция гарантированно не пуста, и тип результата совпадает с типом элементов, reduce читается лучше. Не ус

Блоги

Рекомендаций по книгам