Map, CompactMap и FlatMap в Swift



Книга Map, CompactMap и FlatMap в Swift

В типичном приложении работа с массивами  —  вполне обыденное дело. Нам нередко приходится перебирать все элементы, применяя к ним необходимые изменения.


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


Map


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


Для наглядности разберем пример. Предположим, что нужно создать игру, где игроку даются анаграммы имен героев Marvel, и он должен угадать по ним персонажа. У нас есть список имен, к каждому элементу которого нужно применить следующие шаги.


  1. Перевести имена в нижний регистр.
  2. Удалить пробелы.
  3. Перемешать буквы.

Если не использовать map, а попытаться обойтись forEach, то придется для каждого шага создавать пустой массив, применять действие к каждому элементу и добавлять результат. Три массива, слишком много всего, что усложнит чтение и понимание кода. Посмотрим, насколько этот подход оказывается многословен:


let heroes = ["Iron Man", "Spiderman", "Star Lord", "Black Widow"]

var noWhitespaceHeroes = [String]()
heroes.forEach { hero in
let noWhitespaceHero = hero.replacingOccurrences(of: " ", with: "")
noWhitespaceHeroes.append(noWhitespaceHero)
}
// noWhitespaceHeroes = ["IronMan", "Spiderman", "StarLord", "BlackWidow"]

var lowercasedHeroes = [String]()
noWhitespaceHeroes.forEach { hero in
let lowercasedHero = hero.lowercased()
lowercasedHeroes.append(lowercasedHero)
}
// lowercasedHeroes = ["ironman", "spiderman", "starlord", "blackwidow"]

var heroAnagrams = [String]()
lowercasedHeroes.forEach { hero in
let heroAnagram = String(Array(hero).shuffled())
heroAnagrams.append(heroAnagram)
}
// пример heroAnagrams: ["oranimn", "arisendmp", "sraotlrd", "wlcwabiokd"]

Если же задействовать map, то код получится намного проще и понятнее  —  подобно магии!


let heroes = ["Iron Man", "Spiderman", "Star Lord", "Black Widow"]

let heroAnagrams = heroes.map {
String(Array($0.replacingOccurrences(of: " ", with: "")
.lowercased()
.shuffled())
)
}

Как видите, читать его чрезвычайно легко, даже не зная все внутренние шаги.


CompactMap


CompactMap работает аналогично map, но имеет дополнительный бонус —  удаляет все элементы nil, создаваемые трансформацией.


Рассмотрим пример:


let urlStrings = [
"https://www.marvel.com",
"https://www.😈.com",
"https://www.dccomics.com"
]

let urls = urlStrings.compactMap { URL(string: $0) }
// urls = [https://www.marvel.com, https://www.dccomics.com]

Очевидно, что иконка не способна создать валидный url. С помощью compactMap можно без проблем отфильтровать, не прибегая к дополнительным проверкам.


CompactMap также предоставляет отличный способ для фильтрации списка с целью удаления всех значений nil. Я недавно работал над приложением, в котором все метки UI в одном из представлений управлялись данными, получаемыми с серверами. Если значение присутствовало, нужно было показывать метку, если нет  —  метка скрывалась, не оставляя пустого места.


Все работало динамически в UITableView, так что мне нужно было лишь добавлять или не добавлять элемент в источник данных.


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


let fields = ["John", "Appleseed", nil, "Cupertino"]
let items = fields.compactMap { $0 }
// items = ["John", "Appleseed", "Cupertino"]

Обратите внимание на одну деталь в этом примере. Массив fields имеет тип [String?], а массив items  —  [String], так что возвращаемый тип compacMap никогда не будет optional. Изумительно!


FlatMap


Последний тип карты  —  это функция, которая уплощает массив массивов. Она одним действием преобразует набор массивов в один. Как-то так:


[[1, 1, 1], [2, 2, 2], [3, 3, 3]] -> [1, 1, 1, 2, 2, 2, 3, 3, 3]

Разберем пример.


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


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


Рассмотрим код:


struct HeroTeam {
let name: String
let heroes: [Hero]
}

struct Hero {
let name: String
let enemiesDefeated: [Int]
}

extension Array where Element == Int {
var mean: Double {
return Double(self.reduce(0, +)) / Double(self.count)
}
}

let hero01 = Hero(name: "Iron Man", enemiesDefeated: [12, 14, 8, 19])
let hero02 = Hero(name: "Captain America", enemiesDefeated: [8, 20, 21])
let hero03 = Hero(name: "The Hulk", enemiesDefeated: [12, 25, 19, 28, 32])

let team = HeroTeam(name: "Super Team", heroes: [hero01, hero02, hero03])

let averageDefeated = team.heroes
.compactMap { $0 }
.flatMap { $0.enemiesDefeated }
.mean
// averageDefeated = 18.166666666666668

// Массив массивов до применения flatMap = [[12, 14, 8, 19], [8, 20, 21], [12, 25, 19, 28, 32]]

Вся магия происходит на строке 25. Заметьте, что я для вычисления среднего использовал расширение (строка 26), а не функцию vDSP.mean из фреймворка Accelerate, чтобы сохранить аккуратный стиль функционального программирования.


И еще кое что


Если вы внимательно читали начало статьи, то могли заметить, что я писал не о простых массивах, а о коллекциях. Как думаете, почему? Потому что, функции map также применимы и к словарям! Хотя для этого нужно быть более внимательным:


let heroInfo = [
"firstName":"Anthony Edward",
"lastName" : "Stark",
"nickname": "Iron Man",
"superPowers": "Genius, billionaire, playboy, philanthropist"
]

let infos = heroInfo.map { key, value in
[key: value]
}
// infos = [["lastName": "Stark"], ["nickname": "Iron Man"], ["firstName": "Anthony Edward"], ["superPowers": "Genius, billionaire, playboy, philanthropist"]]

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


Также в Swift для всех трех вышеприведенных примеров есть разные версии карт, с помощью которых можно реализовать эту задачу. Просто используйте вариант mapValues:


let heroInfo = [
"firstName":"Anthony Edward",
"lastName" : "Stark",
"nickname": "Iron Man",
"superPowers": "Genius, billionaire, playboy, philanthropist"
]

let infoUppercased = heroInfo.mapValues{ $0.uppercased() }
// infoUppercased = ["nickname": "IRON MAN", "superPowers": "GENIUS, BILLIONAIRE, PLAYBOY, PHILANTHROPIST", "lastName": "STARK", "firstName": "ANTHONY EDWARD"]

Теперь мы получили [String: String], а не [[String: String}}, как в предыдущем примере.


Успешного вам написания кода!


464   0  

Comments

    Ничего не найдено.