obo.dev

Функции высшего порядка (Function Higher Order)

11 Dec 2022

Функции Высшего порядка (Function Higher Order)

В арсенале Swift есть полезные функции для преобразования коллекций без использования циклов, которые позволяют использовать минимум кода.

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

Функция высшего порядка — это функция, которая принимает одну или несколько функций в качестве аргументов или возвращает функцию в качестве результата. Вот несколько быстрых функций высшего порядка — forEach, map, compactMap, flatMap, filter, reduce, sort и sorted.

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

  • Функция forEach() перебирает значения коллекции и ничего не возвращает;
  • Функция filter() возвращает значения, если необходимое условие истинно;
  • Функция reduce() превращает коллекцию в одно значение;
  • Функции sort(by:) и sorted(by:) сортируют коллекцию;
  • Функции map, flatMap и compactMap преобразуют элементы коллекции.

Функция forEach()

Функция forEach() перебирает все элементы массива и ничего не возвращает. Необходимые действия с перебираемыми элементами необходимо указать в замыкании.

В обычном случае, перебор коллекции выполняется циклом for-in:

let coins = [1, 5, 2, 10, 6]
for coin in coins {
    print("\(coin)$", terminator: " ")
}

// Output
// 1$ 5$ 2$ 10$ 6$

В приведенном выше коде был создан массив coins. Далее, массив coins был перебран циклом for-in. В результате работы цикла, к каждому элементу в конце был добавлен символ $и даный результат был выведен в консоль.

Теперь, тот же перебор, но функцией forEach():

let coins = [1, 5, 2, 10, 6]
coins.forEach { (coin) in
    print("\(coin)$", terminator: " ")
}

// Output
// 1$ 5$ 2$ 10$ 6$

Используется forEach() для перебора всех монет, добавления $ к каждому элементу и его вывода.

Теперь, тот же перебор с функцией forEach(), но используя сокращенный синтаксис замыканий:

let coins = [1, 5, 2, 10, 6]
coins.forEach { print("\($0)$", terminator: " ") }

// Output
// 1$ 5$ 2$ 10$ 6$

Здесь имя параметра замыкания coin было заменено на $0.

Функция forEach() работает так же, как цикл for-in, но основная разница в том, что нельзя использовать операторы break и continue для выхода из замыкания в forEach().

Функция filter()

Функция filter(_:) перебирает каждый элемент в коллекции и возвращает новую коллекцию, которая содержит только те элементы, которые удовлетворяют поставленным условиям.

Это все равно, что применять if условие к коллекции и сохранять только те значения, которые проходят проверку.

Пример:

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
var coinsWithValueLessThanSix = [Int]()
for element in coins {
    if element < 6 {
        coinsWithValueLessThanSix.append(element)
    }
}
print(coinsWithValueLessThanSix)

// Output
// [1, 5, 2, 2, 4]

В этом примере создана переменная coinsWithValueLessThanSix, которая является пустым массивом целых чисел. Затем, циклом перебираются элементы массива coins. И если они соответствуют условию (меньше 6), то они добавляются в массив coinsWithValueLessThanSix.

Теперь тот же пример, но с функцией filter(_:):

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
let coinsWithValueLessThanSix = coins.filter { (coin) -> Bool in
    coin < 6
}
print(coinsWithValueLessThanSix)

// Output
// [1, 5, 2, 2, 4]

Функция filter(_:) берет по очереди каждый элемент из коллекции coins и подставляет его в замыкание. Если вернётся значение замыкания true, то функция filter(_:) выполнит добавление элемента в массив coinsWithValueLessThanSix.

Или можно записать ещё короче:

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
let coinsWithValueLessThanSix = coins.filter { $0 < 6 }
print(coinsWithValueLessThanSix)

// Output
// [1, 5, 2, 2, 4]

Обратите внимание, что при использовании функции filter(), новый массив coinsWithValueLessThanSix можно объявить - как константу, так как все операции выполняются при её инициализации.

Ещё пример:

let values = [11, 13, 14, 6, 17, 21, 33, 22]
var even = [Int]()
for element in values {
	if element.isMultiple(of: 2) {
		even.append(element)
	}
}
print(even)

// Output
// [14, 6, 22]

В приведенном примере фильтруются четные числа из массива values. Функция isMultiple(of:) возвращает true, когда текущее значение кратно 2. В противном случае возвращается false.

Теперь применим функцию filter(_:):

let values = [11, 13, 14, 6, 17, 21, 33, 22]
let even = values.filter({ (value: Int) -> Bool in
    return value.isMultiple(of: 2)
})
 
print(even)

// Output
// [14, 6, 22]

Более короткая запись:

let values = [11, 13, 14, 6, 17, 21, 33, 22]
let even = values.filter { $0.isMultiple(of: 2) }
print(even)

// Output
// [14, 6, 22]

В отличие от функций map(_:) и reduce(_:_:) - функция filter(_:) должна возвратить либо true либо false. Когда замыкание возвращает true, значения сохраняются, а когда возвращается false, значения опускаются.

Функция reduce()

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

Функция reduce(_:, _:) принимает два параметра: начальное значение и обрабатывающее замыкание.

Каким образом можно перейти от множества значений в коллекции к одному значению? Вот несколько примеров:

  • Подсчет суммы нескольких значений: 3 + 4 + 5 = 12;
  • Конкатенация коллекции строк: [«Дом», «Комната», «Стул»] = «Дом, Комната, Стул»;
  • Усреднение набора значений: (7 + 3 + 10) / 3 = 7/3 + 3/3 + 10/3 = 6.667.

Можно решить любую из этих проблем с помощью цикла for, но с помощью функции reduce(_:, _:) код будет гораздо проще и компактней.

Пример. Необходимо получить сумму элементов коллекции:

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
var sumOfCoins = 0
for element in coins {
    sumOfCoins += element
}

print(sumOfCoins)

// Output
// 62

Здесь была инициализирована переменная sumOfCoins со значением 0. Дальше, циклом перебирается массив coins и каждый элемент добавляется к sumOfCoins.

Теперь применим функцию reduce():

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]

let sumOfCoins = coins.reduce(0) { (result, coin) -> Int in
    result + coin
}

print(sumOfCoins)

// Output
// 62

Функция reduce() берёт своё изначальное значение 0 и передает его в замыкание в качестве первого параметра result. Потом берёт первый элемент из массива и передаёт его в качестве второго параметра замыкания coin. После выполнения замыкания, функция reduce() заменяет своё изначальное значение на результат замыкания. Дальше, этот процесс повторяется для остальных элементов массива. После обработки последнего элемента массива coins, функция reduce() передаёт сохранённое значение в константу sumOfCoins.

Таким образом, для функции reduce() необходимо два параметра: изначальное значение и замыкание, которое будет обрабатывать входящие параметры.

Код можно сократить, использую синтаксис замыканий:

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]

let sumOfCoins = coins.reduce(0) { $0 + $1 }

print(sumOfCoins)

// Output
// 62

Данное замыкание имеет тип (Int, Int) -> (Int). То есть его можно заменить на оператор + - который является таким же по типу данных. В итоге, код можно сократить ещё:

let coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]

let sumOfCoins = coins.reduce(0, +)

print(sumOfCoins)

// Output
// 62

Функция принимает два аргумента — начальное значение и замыкание. В приведенном выше коде, в качестве замыкания - используется оператор +.

Данный арифметический оператор складывает два значения и производит их сумму. Например, для целых чисел он объявлен так:

static func + (lhs: Int, rhs: Int) -> Int

И значит, он имеет функциональный тип данных: (Int, Int) -> Int.

Больше про операторы можно узнать в отдельной статье. См. ссылку внизу.

Функция reduce(_:_:) имеет свои особенности:

  • Функция принимает два параметра: начальное значение и замыкание;
  • Замыкание также принимает два параметра: текущий результат и новое значение.

Также можно написать свое собственное замыкание:

let values = [7.0, 3.0, 10.0]
let average = values.reduce(0.0) { $0 + $1 } / Double(values.count)
print(average)

// Output
// 6.666666666666667

В приведенном выше примере рассчитывается среднее значение из трех чисел. Все значения имеют тип Double. Сначала складываются все числа, а затем результат делится их на общее количество.

let values = [7, 3, 10]
let sum = values.reduce(0) {
    print("\($0) + \($1) = \($0 + $1)")
    return $0 + $1
}
 
print(sum)

// Output
// 0 + 7 = 7
// 7 + 3 = 10
// 10 + 10 = 20
// 20

В приведенном примере можно четко видеть 2 параметра замыкания — $0 и $1:

  • $0 - в первой итерации - это значение первого параметра reduce(0), в последующих итерациях - результат предыдущей итерации;
  • $1 - элементы values, которые перебираются в итерациях.

Мы начинаем с 0 (первый параметр reduce(0)), потом добавляем 7 (первый элемент массива values). На следующем шаге мы берем 7 (текущее значение после первой итерации 0 + 7) и добавляем 3 (следующее значение в массиве values). Последняя итерация - 10 + 10. Теперь в константе sum храниться значение 20.

Функции sort(by:) и sorted(by:)

Функции sort(by:) и sorted(by:) сортируют коллекцию по заданным в замыкании условиям. Разница между этими функциями такая:

  • функция sort(by:) сортирует все элементы текущего массива в соответствии с условием, записанным внутри тела замыкания.
  • функция sorted(by:) отсортирует все элементы в соответствии с условием, написанным внутри тела замыкания, и вернет новый массив.

То есть, функция sort(by:) изменяет текущий массив, а функция sorted(by:) - текущий массив не затрагивает, а создаёт на его основе новый.

Алгоритм сортировки этих функций - один из пузырьковой сортировки: берется первая пара элементов массива и проверяется условие сортировки, указаной в замыкании:

  • если возвращается true, то происходит переход к следующей итерации и паре элементов;
  • если возвращается false, то элементы меняются местами и происходит переход к следующей итерации и паре элементов;

Так перебирается весь массив. После этого весь цикл проверки повторяется. И если опять на какой-то паре элементов возвращается false - все операции повторяются.

Когда цикл вернет на всех итерациях true - выполнение сортировки останавливается и возвращается результат сортировки.

Пример сортировки пузырьком:

Массив: 1, 3, 4, 2

Условие: по возрастанию

Циклы Итерации Массив Пара элементов Сравнение Результат
Цикл 1 Итерация 1 1, 3, 4, 2 1 3 true 1, 3, 4, 2
Цикл 1 Итерация 2 1, 3, 4, 2 3 4 true 1, 3, 4, 2
Цикл 1 Итерация 3 1, 3, 4, 2 4 2 false 1, 3, 2, 4
Цикл 2 Итерация 1 1, 3, 2, 4 1 3 true 1, 3, 2, 4
Цикл 2 Итерация 2 1, 3, 2, 4 3 2 false 1, 2, 3, 4
Цикл 2 Итерация 3 1, 2, 3, 4 3 4 true 1, 2, 3, 4
Цикл 3 Итерация 1 1, 2, 3, 4 3 4 true 1, 2, 3, 4
Цикл 3 Итерация 2 1, 2, 3, 4 3 4 true 1, 2, 3, 4
Цикл 3 Итерация 3 1, 2, 3, 4 3 4 true 1, 2, 3, 4

В третьем цикле в каждой итерации получено значение true согласно условиям сортировки. Значит сортировка заканчивается и возвращается результат сортировки: 1, 2, 3, 4.

Пример с функцией sorted():

var coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
let sortCoins = coins.sorted { (a, b) -> Bool in
    a > b
}
print(sortCoins)

// Output
// [15, 10, 10, 7, 6, 5, 4, 2, 2, 1]

В приведенном выше коде был создаете массив coins. Дальше, массив сортируется в порядке убывания и создаётся массив с именем sortCoins.

Код можно сократить, использую синтаксис замыканий:

var coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
let sortCoins = coins.sorted { $0 > $1 }
print(sortCoins)

// Output
// [15, 10, 10, 7, 6, 5, 4, 2, 2, 1]

Так как функция sorted() принимает в качестве параметра замыкание:

sorted(by: (Self.Element, Self.Element) -> Bool)

то код выше можно переписать так:

var coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
let sortCoins = coins.sorted(by: >)
print(sortCoins)

// Output
// [15, 10, 10, 7, 6, 5, 4, 2, 2, 1]

Пример с применением функции sort(by:), которая меняет изначальный массив:

var coins = [1, 5, 2, 10, 6, 2, 7, 4, 10, 15]
coins.sort(by: >)
print(coins)

// Output
// [15, 10, 10, 7, 6, 5, 4, 2, 2, 1]

Принцип действия функции sort() такой же, как и у sorted(), только, как сказано выше, она применяется к текущему массиву, изменяя его, а не создает новый.


Функции map, flatMap и compactMap в программировании на Swift

В этом разделе будут рассмотрены функции map(:)**, **flatMap(:) и compactMap(_:).

Функция map()

Функция map(_:) выполняет последовательное преобразование каждого элемента в коллекции и возвращает коллекцию элементов, к которым была применено преобразование.

let numbers = [2, 3, 4, 5]
let result = numbers.map({ $0 * $0 })
 
print(result)

// Output
// [4, 9, 16, 25]

Сначала мы создаем массив numbers с несколькими значениями типа Int. Затем вызывается функция map(_:) и ее результат присваивается константе result.

Функция имеет один параметр — замыкание, которое возвращает результат $0 * $0.

$0 соответствует первому параметру замыкания или каждому числу в нашей коллекции.

В этом примере, берётся по очереди каждый элемент коллекции (в данном примере - массива) и умножается каждое число на само себя. Таким образом, рассчитывая его квадрат. В итоге, получается новый массив - происходит трансформация одного массива в другой. При этом изначальный массив - numbers - не меняется.

Аналогичное преобразование массива можно также выполнить с помощью цикла for:

let numbers = [2, 3, 4, 5]
var result = [Int]()
 
for number in numbers {
    result += [number * number]
}
 
print(result)
// [4, 9, 16, 25]

То есть с помощью функции map(_:) входной массив чисел преобразуется в другой массив чисел в последовательности:

Numbers Transformation Result
2 2 * 2 4
3 3 * 3 9
4 4 * 4 16
5 5 * 5 25

Разберем еще один пример.

let celsius = [-5.0, 10.0, 21.0, 33.0, 50.0]
let fahrenheit = celsius.map { $0 * (9/5) + 32 }
print(fahrenheit)

// Output
// [23.0, 50.0, 69.80000000000001, 91.4, 122.0]

Данный результат так же может быть получен с помощью цикла for:

let celsius = [-5.0, 10.0, 21.0, 33.0, 50.0]
var fahrenheit: [Double] = []
 
for value in celsius {
    fahrenheit += [value * (9/5) + 32]
}
 
print(fahrenheit)

// Output
// [23.0, 50.0, 69.8, 91.4, 122.0]

Также можно записать весь код в одну строку:

[-5.0, 10.0, 21.0, 33.0, 50.0].map{ $0 * (9/5) + 32 }

Функция map(_:) преобразует один массив в другой, применяя замыкание к каждому элементу в массиве. Замыкание принимает входное значение в градусах Цельсия и возвращает значение в градусах Фаренгейта. Полученный массив построен из преобразованных значений — $0 * (9/5) + 32.

Так, а что под капотом?!

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

Начнём с менее краткой альтернативы:

let celsius = [-5.0, 10.0, 21.0, 33.0, 50.0]

let fahrenheit = celsius.map({ (value: Double) -> Double in
    return value * (9/5) + 32
})

print(fahrenheit)

// Output
// [23.0, 50.0, 69.8, 91.4, 122.0]

Первая часть замыкания, начиная с {. Далее, код (value: Double) -> Double in указывает, что это замыкание имеет один параметр типа Double, и замыкание также возвращает значение типа Double. Тело замыкания (return value * (9/5) + 32), начиная с return, возвращает результат вычисления градусов по Цельсию в градусах Фаренгейта по указанной формуле.

Так как последний параметр функции - это замыкание, то круглые скобки после функции () можно убрать:

...

let fahrenheit = celsius.map { (value: Double) -> Double in
    return value * (9/5) + 32
}

...

() -> in — эта часть также может быть опущена, поскольку Swift может сделать вывод, что вы используете один параметр типа Double в качестве входных данных, и ожидается, что он вернет также тип Double.

...

let fahrenheit = celsius.map { value in
    return value * (9/5) + 32
}

...

Теперь, можно удалить имя входящего параметра и заменить его на $0 - сокращение $0 для передаваемого параметра.

...

let fahrenheit = celsius.map {
    return $0 * (9/5) + 32
}

...

Так как код в одну строку и происходит возврат значения - то оператор return также может быть опущен.

...

let fahrenheit = celsius.map {
    $0 * (9/5) + 32
}

...

Или в одну строку:

...

let fahrenheit = celsius.map { $0 * (9/5) + 32 }

...

Функция flatMap()

Функция flatMap(:)** в отличие от **map(:) всегда возвращает только одномерный массив:

let numbers = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let result = numbers.flatMap({ $0 })
 
print(result)

// Output
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

Приведенный выше код состоит из 3 вложенных массивов целых чисел, каждый из которых содержит по 3 числа.

$0 возвращает первый аргумент замыкания, то есть отдельно вложенные массивы. Никаких преобразований при этом не происходит. Возвращаемый результат уже не содержит вложенных массивов.

Давайте посмотрим на другой пример.

Представьте, что вы работаете с 4 группами жирафов и хотите создать одну группу жирафов, которая выше определенной высоты:

let giraffes = [[5, 6, 9], [11, 2, 13, 20], [1, 13, 7, 8, 2]]
let tallest = giraffes.flatMap({ $0.filter({ $0 > 10 }) })
 
print(tallest)

// Output
// [11, 13, 20, 13]

В приведенном выше коде функция filter(_:) вызывается для каждого вложенного массива. Полученные массивы сведены в один одномерный массив и присвоены константе tallest.

Здесь, функция filter(_:) отфильтровывает те параметры, которые соответствуют условию $0 > 10 и возвращает их в функцию flatMap(_:).

Если бы в данном коде использовали бы функцию map(_:) вместо flatMap(_:), то получили бы несколько массивов:

let giraffes = [[5, 6, 9], [11, 2, 13, 20], [1, 13, 7, 8, 2]]
let tallest = giraffes.map({ $0.filter({ $0 > 10 }) })
 
print(tallest)

// Output
// [[], [11, 13, 20], [13]]

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

let numbers = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let result = numbers.flatMap({ $0 * 2 })  // Cannot convert value of type '[Int]' to expected argument type 'Int'

В приведенном выше примере $0 ссылается на отдельные массивы внутри массива numbers. Умножить массив на два невозможно, поэтому данный код не работает.

Функция compactMap()

Функция compactMap(_:) удаляет значения nil из массива. Она очень полезна при работе с опционалами.

let numbers = ["5", "42", "nine", "100", "Bob"]
let result = numbers.compactMap({ Int($0) })
 
print(result)

// Output
// [5, 42, 100]

Наиболее важной частью кода является Int($0). Мы берем каждую строку из массива и пытаемся ее преобразовать в целое число. Инициализатор Int() - проваливающийся (failable). То есть, он возвращает опциональные значения типа Int?. В результате, тип возвращаемого преобразования — массив опциональных целых чисел [Int?].

[Optional(5), Optional(42), nil, Optional(100), nil]

Функция compactMap(_:) автоматически удаляет nil элементы из возвращаемого массива. Таким образом, тип возвращаемого значения больше не является опциональным.

[5, 42, 100]

Приведенный код имеет тип [Int]. Если использовалась бы функция map(_:), то возвращаемый тип был бы [Int?] и нам бы понадобился дополнительный шаг, чтобы извлечь опциональные значения из массива.

Чем могут быть полезны данные функции?

Кратко рассмотрим описанные функции высшего порядка и их цели:

  • Функция map(_:) применяет замыкание к каждому элементу коллекции и тем самым ее преобразует в новую коллекцию;
  • Функция flatMap(_:) делает то же самое, но при этом возвращает одномерный массив;
  • Функция flatMap(_:) делает то же самое, но при этом удаляет все значения nil из коллекции.

Чем данные функции могут быть полезны?

  • Данные функции позволяют сделать код более кратким по сравнению с циклами, потому что больше не нужны временные переменные и многострочный код цикла for;
  • Можно написать вызов функции в одну строку, что делает код более читабельным;
  • Подобные функции могут быть объединены в одну цепочку, поэтому можно использовать несколько последовательных преобразований к одной коллекции.

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

К примеру, функция flatMap(_:) предоставляет наиболее практичный вариант для работы с входными значениями коллекции, которые сгруппированы или вложены друг в друга, но при этом требуемое выходное значение должно быть одномерным.

Например, в музыкальном приложении могут быть 3 типа массивов: песни, исполнители и плейлисты. При этом каждый из данных типов имеет свойство isFavorite. Можно объединить все три массива в один массив, вызвать функцию flatMap(_:) и выбирать песни, исполнители или плейлисты, для которых значение isFavorite равно true. В итоге получим одномерный массив с нужными значениями.

Представьте, что создаете приложение для социальных сетей. И нужно создать ленту новостей для каждого пользователя. Вы используете 3 запроса, чтобы выбрать идентификаторы сообщений для пользователя. Например, из публикаций подписчиков, рекламных объявлений и популярных тем.

  • можно использовать функцию map(_:), чтобы получить посты через значения ID;
  • можно использовать функцию flatMap(_:), чтобы объединить три группы постов в одну коллекцию;
  • функция compactMap(_:) позволит отфильтровать полученные посты.

Группировка нескольких функций

Вы также можете объединять несколько функций высшего порядка.

Допустим, у нас есть класс студентов. Вы знаете год, когда родился каждый студент. Вы хотите рассчитать совокупный возраст всех учащихся, родившихся в 2000 году или позже.

let now = 2020
let years = [1989, 1992, 2003, 1970, 2014, 2001, 2015, 1990, 2000, 1999]
let sum = years.filter({ $0 >= 2000 }).map({ now - $0 }).reduce(0, +)
print(sum)

// Output
// 67

В приведенном выше примере кода используется связывание нескольких функций. Данный код использует результат одной функции в качестве входных данных для другой.


Еще полезные ссылки