Перейти к содержимому
Теория · Замыкания · урок 4 из 8

map, filter, reduce

Представьте повара у ленты с продуктами. Вы отдаёте ему карточку с рецептом, и он применяет её к каждому продукту по очереди. Как идти вдоль ленты, повар знает сам, а вы решаете только, что сделать с одним продуктом.

С массивами чаще всего решают три задачи:

  • превратить каждый элемент во что-то другое;
  • оставить только подходящие элементы;
  • собрать все элементы в одно значение.

Каждую можно решить циклом for-in, но для каждой в Swift есть готовый метод. Метод сам обходит массив, а что делать с элементом, вы говорите ему замыканием:

SWIFT
let prices = [120, 45, 300, 80]
let doubled = prices.map { $0 * 2 }
let cheap = prices.filter { $0 < 100 }
let total = prices.reduce(0, +)
print(doubled) // [240, 90, 600, 160]
print(cheap) // [45, 80]
print(total) // 545

Все три метода — функции высшего порядка: они принимают функцию. Разберём каждый.

Три метода

map — превратить каждый элемент

map вызывает замыкание для каждого элемента и собирает результаты в новый массив той же длины. prices.map { $0 * 2 } даёт каждую цену, умноженную на два.

Тип элементов при этом может поменяться. Замыкание { "\($0) ₽" } превратило бы массив чисел в массив строк.

filter — оставить подходящие

Замыкание для filter возвращает Bool. Элемент попадает в новый массив, если замыкание вернуло true, и пропускается, если вернуло false. prices.filter { $0 < 100 } оставляет цены меньше ста — [45, 80]. Порядок элементов сохраняется.

reduce — собрать в одно значение

reduce(0, +) получает два аргумента:

  • начальное значение 0;
  • функцию, которая соединяет накопленное значение со следующим элементом; здесь это оператор +.

Давайте посмотрим, как reduce проходит по prices:

  1. Начало — 0.
  2. 0 + 120 даёт 120.
  3. 120 + 45 даёт 165.
  4. 165 + 300 даёт 465.
  5. 465 + 80 даёт 545 — это и есть результат.

Запись prices.reduce(0) { $0 + $1 } делает то же самое: в замыкании $0 — накопленное значение, а $1 — очередной элемент.

Здесь картинка с поваром неточна. Повар режет те самые продукты, что лежат на ленте. А map, filter и reduce исходный массив не трогают: prices после них остаётся прежним.

Цепочка вызовов

map и filter возвращают новый массив, поэтому следующий метод можно вызвать прямо у результата. Порядок в цепочке важен:

SWIFT
let a = prices.filter { $0 < 100 }.map { $0 * 2 }
let b = prices.map { $0 * 2 }.filter { $0 < 100 }
print(a, b) // [90, 160] [90]

Разберём обе цепочки:

  1. В a сначала работает filter и оставляет [45, 80], а потом map удваивает их: [90, 160].
  2. В b сначала map удваивает все цены: [240, 90, 600, 160]. Потом filter оставляет из них числа меньше ста: [90].

Цепочку читают слева направо. Каждый следующий метод работает с результатом предыдущего, а не с исходным массивом.

Этот вопрос часто задают на собеседовании: чем отличаются map, filter и reduce? Ответ:

  • map возвращает массив той же длины, где каждый элемент преобразован;
  • filter возвращает массив из тех элементов, для которых замыкание вернуло true;
  • reduce возвращает одно значение, собранное из всех элементов.

Частая ошибка

Хотели убрать из массива отрицательные температуры и написали в filter условие для них:

SWIFT
let temps = [3, -2, 7, -5]
let positive = temps.filter { $0 < 0 } // ✗
print(positive) // [-2, -5]

Ошибки нет ни при компиляции, ни при запуске, но в positive оказались как раз отрицательные числа. filter оставляет элементы, для которых замыкание вернуло true, а не убирает их. Пишите в замыкании условие для тех элементов, которые должны остаться: temps.filter { $0 > 0 } даёт [3, 7].

Главное

  • map вызывает замыкание для каждого элемента и возвращает новый массив той же длины.
  • filter оставляет элементы, для которых замыкание вернуло true.
  • reduce(0, +) собирает массив в одно значение, начиная с 0.
  • Исходный массив ни один из этих методов не меняет, а в цепочке каждый метод работает с результатом предыдущего.