Объясни fold, reduce, scan — чем они отличаются?

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

reduce

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

Сигнатура:

def reduce\[A1 >: A\](op: (A1, A1) => A1): A1

Пример:

val list = List(1, 2, 3, 4)
val result = list.reduce(_ + \_) // 10

reduce работает только с непустыми коллекциями. Если список пуст, будет выброшено исключение.

Поток операций:

(((1 + 2) + 3) + 4)  10

fold

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

Сигнатура:

def fold\[B\](z: B)(op: (B, A) => B): B
  • z — начальное значение типа B

  • op — функция, которая принимает аккумулятор и элемент

Пример:

val list = List(1, 2, 3, 4)
val result = list.fold(0)(_ + \_) // 10

Поток операций:

(((0 + 1) + 2) + 3) + 4  10

Пример с другим типом:

val result = list.fold("Start:")((acc, x) => acc + " " + x)
// "Start: 1 2 3 4"

Вариации:

  • foldLeft — проход слева направо (обычно используемый)

  • foldRight — справа налево (может быть менее эффективным из-за рекурсии)

List(1, 2, 3).foldLeft(0)(_ - \_) // (((0 - 1) - 2) - 3) = -6
List(1, 2, 3).foldRight(0)(_ - \_) // (1 - (2 - (3 - 0))) = 2

scan

scan — это то же самое, что fold, но возвращает все промежуточные значения аккумулятора, а не только итоговое.

Сигнатура:

def scan\[B\](z: B)(op: (B, A) => B): collection.Seq\[B\]
  • Возвращает коллекцию, включающую z и каждый шаг накопления.

Пример:

val list = List(1, 2, 3)
val result = list.scan(0)(_ + \_)
// Vector(0, 1, 3, 6)
  • Сначала 0 (начальное значение)

  • 0 + 1 = 1

  • 1 + 2 = 3

  • 3 + 3 = 6

Применение:

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

Сравнение всех трёх:

Метод Начальное значение Результат Пустая коллекция
reduce ❌ (нет) Одно итоговое значение Ошибка
--- --- --- ---
fold ✅ (обязательное) Одно итоговое значение Всегда безопасно
--- --- --- ---
scan ✅ (обязательное) Список промежуточных шагов Всегда безопасно
--- --- --- ---

Ещё примеры

reduce:

List(5, 2, 8).reduce((a, b) => if (a > b) a else b) // 8

fold:

List("a", "b", "c").fold("")(_ + \_) // "abc"

scan:

List(1, 2, 3, 4).scan(1)(_ \* \_) // Vector(1, 1, 2, 6, 24)

Резюме различий на коде

val data = List(1, 2, 3)
// reduce
val r = data.reduce(_ + \_) // 6
// fold
val f = data.fold(0)(_ + \_) // 6
// scan
val s = data.scan(0)(_ + \_) // Vector(0, 1, 3, 6)

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