$ sudo teach IT

Зачем нужны функции высшего порядка

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

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

Прежде чем перейти к примерам, — про короткую запись замыкания { $0 }. Когда вы передаёте в такой метод короткое выражение, Swift разрешает не писать имя параметра и in, а обращаться к первому аргументу как к $0, ко второму — как к $1, и так далее. Это просто сокращённая запись, не отдельный механизм — устройство замыканий подробно разберём отдельно, здесь достаточно понимать, что $0 — это «тот единственный элемент, который сейчас обрабатывается».

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

map применяет переданное замыкание к каждому элементу коллекции и возвращает новый массив того же размера, где на месте каждого элемента — результат замыкания. Исходная коллекция не меняется.

let prices = [100, 250, 400]
let withDiscount = prices.map { $0 - 50 }
print(withDiscount)

Построчно: prices.map { ... } берёт каждое число из prices по очереди, подставляет его вместо $0 в выражение $0 - 50 и кладёт результат в новый массив. На выходе — [50, 200, 350]: три элемента на входе, три на выходе, порядок сохранён.

filter — оставить только нужное

filter проверяет каждый элемент замыканием, которое возвращает Bool, и оставляет в новом массиве только те элементы, для которых замыкание вернуло true. Размер результата заранее не известен — он может быть от нуля до размера исходной коллекции.

let scores = [40, 95, 60, 88, 30]
let passed = scores.filter { $0 >= 60 }
print(passed)

scores.filter { $0 >= 60 } проходит по каждому числу и спрашивает: «оно не меньше 60?». Число остаётся в passed, только если ответ true. Результат — [95, 60, 88]: элементы, не прошедшие проверку, просто исчезли, порядок оставшихся не изменился.

reduce — свернуть коллекцию в одно значение

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

let numbers = [1, 2, 3, 4]
let total = numbers.reduce(0) { accumulator, item in
    accumulator + item
}
print(total)

Разберём по шагам. Начальное значение — 0, это то, с чего стартует накопитель ещё до первого элемента. Дальше reduce идёт по массиву: на первом шаге accumulator равен 0, item — 1, замыкание возвращает 1 — это новый accumulator. На втором шаге accumulator — 1, item — 2, результат 3. Так продолжается до конца массива, и последнее возвращённое значение и есть итог — 10. Начальное значение — это не «первый элемент массива», это стартовая точка накопителя, и её можно выбрать любой: если бы вы начали с 100, итог был бы 110.

Для сложения и умножения есть ещё более короткая запись — можно передать сам оператор вместо замыкания, потому что + — это тоже функция, принимающая два аргумента:

let total2 = numbers.reduce(0, +)
print(total2)

Это делает ровно то же самое: 0 — начальное значение, + — правило объединения накопителя с очередным элементом.

compactMap — преобразовать и сразу отбросить пустое

compactMap ведёт себя как map, но ожидает, что замыкание возвращает Optional. Он применяет преобразование к каждому элементу, разворачивает результат, если он не nil, и просто выбрасывает элемент из результата, если преобразование вернуло nil. Итоговый массив может быть короче исходного и содержит уже не опционалы, а развёрнутые значения.

Самый практичный пример — разбор строк, введённых пользователем, в числа: Int(строка) возвращает Optional(Int) и превращается в nil, если строка не является числом.

let input = ["12", "seven", "5", "", "9"]
let parsed = input.compactMap { Int($0) }
print(parsed)

Int($0) пытается превратить каждую строку в число. Для "12" получится Optional(12), для "seven" и пустой строки — nil. compactMap разворачивает успешные значения и молча пропускает nil, поэтому результат — [12, 5, 9]: три числа вместо пяти исходных строк, и это уже обычные Int, без Optional.

flatMap — развернуть вложенность

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

let groups = [[1, 2], [3, 4, 5], [6]]
let flat = groups.flatMap { $0 }
print(flat)

groups — массив массивов. flatMap { $0 } для каждой внутренней группы возвращает её саму, но вместо того, чтобы сложить группы одну в другую (как сделал бы обычный map, получив снова массив массивов), flatMap «расплющивает» их в одну последовательность: [1, 2, 3, 4, 5, 6].

forEach — просто выполнить действие

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

["Аня", "Борис"].forEach { name in
    print("Привет, \(name)!")
}

Здесь важно не путать forEach с map: map отвечает на вопрос «что должно получиться взамен», а forEach — «что нужно сделать с каждым элементом», не создавая ничего нового. Использовать map ради одной только печати, выбрасывая результат, — плохой стиль, для этого и существует forEach.

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

Методы можно соединять один за другим — результат одного сразу становится источником для следующего. Такие цепочки принято читать слева направо, как последовательность шагов обработки:

let bonuses = [10, -5, 20, 0, 15]
let total = bonuses
    .filter { $0 > 0 }
    .map { $0 * 2 }
    .reduce(0, +)
print(total)

Читаем как рецепт: сначала filter оставляет только положительные бонусы — [10, 20, 15], затем map удваивает каждый — [20, 40, 30], затем reduce складывает их в 90. Каждый шаг получает на входе результат предыдущего, и цепочку легко читать и легко расширять — правда, чем она длиннее, тем полезнее иногда разбить её на промежуточные переменные с понятными именами, чтобы не терять нить.

Когда лучше остаться на for-in

Честно: обычный цикл for-in не хуже. Он ничем не проигрывает по скорости и иногда даже понятнее — особенно если внутри цикла несколько разных действий, есть ранний выход через break, или логика ветвится на несколько случаев. Функции высшего порядка хороши, когда задача укладывается в одну понятную операцию — преобразовать, отобрать, свернуть. Если ради «функционального стиля» код превращается в нечитаемую цепочку из пяти методов — лучше вернуться к простому циклу. Выбор — это вопрос читаемости, а не «правильного» и «неправильного» способа.

Частые ошибки

  • Использовать map вместо forEach, когда результат никуда не сохраняется — компилятор не запретит, но это вводит в заблуждение того, кто читает код.
  • Путать начальное значение reduce с первым элементом массива: numbers.reduce(0, +) начинает с нуля независимо от того, что лежит в numbers.
  • Забывать, что filter может вернуть пустой массив — если потом сразу обращаться к первому элементу без проверки, программа упадёт.
  • Применять flatMap там, где нужен обычный map — если замыкание возвращает не коллекцию, а одно значение, flatMap не нужен.
  • Строить длинную цепочку из четырёх-пяти методов подряд там, где простой for-in читался бы понятнее.

Резюме

  • map — превращает каждый элемент во что-то другое, размер результата совпадает с исходным.
  • filter — оставляет элементы, прошедшие условие, размер результата заранее не известен.
  • reduce — сворачивает коллекцию в одно значение через начальное значение и накопитель.
  • compactMap — как map, но отбрасывает nil и разворачивает остальное; удобен для разбора строк в числа.
  • flatMap — превращает коллекцию коллекций в одну плоскую коллекцию.
  • forEach — выполняет действие для каждого элемента, ничего не возвращая.
  • $0, $1 — короткая запись для аргументов замыкания без явного имени.
  • Цепочки читаются слева направо; если цепочка стала длинной и запутанной, обычный for-in — не хуже.

Проверьте себя

4 вопроса

Квадраты чисел

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

Используйте map — она отлично подходит, когда каждому элементу нужно сопоставить ровно одно новое значение.

Сумма чётных чисел

Напишите функцию sumOfEvenNumbers, которая принимает массив целых чисел и возвращает сумму только чётных элементов. Если чётных чисел в массиве нет (или массив пуст), функция должна вернуть 0.

Отберите нужные элементы одним методом и сложите их другим — так вы опишете задачу в два понятных шага вместо ручного цикла со счётчиком.

Сумма чисел из строк

Напишите функцию sumValidNumbers, которая принимает массив строк и возвращает сумму тех строк, которые получается превратить в целое число. Строки, которые числом не являются (включая пустые и с лишними пробелами), нужно просто пропустить, не прерывая работу функции. Если ни одна строка не подошла, верните 0.

Вспомните, что Int(строка) возвращает Optional: он равен числу, если строка действительно им является, и nil иначе.