Привет, Хабр!
Меня зовут Александр Зимин, я iOS-разработчик в Badoo. Это перевод статьи моего коллеги Швиба, в которой он рассказал, что из себя представляла функция flatMap в Swift и почему одну из её перегрузок переименовали в compactMap. Статья полезна как для понимания процессов, происходящих в репозитории Swift и его эволюции, так и для общего развития.
В функциональном программировании есть чёткое определение того, что должна представлять собой функция
Уже на протяжении нескольких версий в Swift есть
Поначалу может быть не так просто понять суть нововведения. Если
Стандартная библиотека Swift до версии 4.1 предоставляла три реализации перегрузки (overloads) для
Давайте пройдёмся по всем трём вариантам и посмотрим, что они делают.
Первая перегрузка предназначена для последовательностей, в которых замыкание берёт элемент этой последовательности и преобразует в другую последовательность.
Это замечательный пример того, как должен работать метод
Вторая перегрузка предназначена для опциональных типов. Если вызываемый вами опциональный тип имеет значение, то замыкание будет вызвано со значением без опциональной обёртки (unwrapped value), и вы сможете вернуть преобразованное опциональное значение.
Третья перегрузка поможет понять, для чего нужен
Но в этом случае не выполняется упорядочивание. Следовательно, эта версия
Это применение
В Swift до версии 4.0 мы бы получили преобразование в
При вызове
Чтобы избежать неправильного использования
Меня зовут Александр Зимин, я iOS-разработчик в Badoo. Это перевод статьи моего коллеги Швиба, в которой он рассказал, что из себя представляла функция flatMap в Swift и почему одну из её перегрузок переименовали в compactMap. Статья полезна как для понимания процессов, происходящих в репозитории Swift и его эволюции, так и для общего развития.
В функциональном программировании есть чёткое определение того, что должна представлять собой функция
flatMap
. Метод flatMap
берёт список и преобразующую функцию (которая для каждого преобразования ожидает получить ноль или больше значений), применяет её к каждому элементу списка и создаёт единый (flattened) список. Такое поведение отличается от простой функции map
, которая применяет преобразование к каждому значению и для каждого преобразования ожидает получить только одно значение.Уже на протяжении нескольких версий в Swift есть
map
и flatMap
. Однако в Swift 4.1 вы больше не можете применять flatMap
к последовательности значений и при этом передавать замыкание, которое возвращает опциональное значение. Для этого теперь есть метод compactMap
.Поначалу может быть не так просто понять суть нововведения. Если
flatMap
хорошо работал, зачем вводить отдельный метод? Давайте разберёмся.Стандартная библиотека Swift до версии 4.1 предоставляла три реализации перегрузки (overloads) для
flatMap
: 1. Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element], где S : Sequence
2. Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
3. Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
Давайте пройдёмся по всем трём вариантам и посмотрим, что они делают.
Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element], где S: Sequence
Первая перегрузка предназначена для последовательностей, в которых замыкание берёт элемент этой последовательности и преобразует в другую последовательность.
flatMap
сводит все эти преобразованные последовательности в финальную последовательность, возвращаемую в качестве результата. Например:let array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let flattened = array.flatMap { $0 } // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Это замечательный пример того, как должен работать метод
flatMap
. Мы преобразуем (map) каждый элемент исходного списка и создаём новую последовательность. Благодаря flatMap
конечный результат представляет собой сплющенную структуру из преобразованных последовательностей.Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Вторая перегрузка предназначена для опциональных типов. Если вызываемый вами опциональный тип имеет значение, то замыкание будет вызвано со значением без опциональной обёртки (unwrapped value), и вы сможете вернуть преобразованное опциональное значение.
let a: Int? = 2
let transformedA = a.flatMap { $0 * 2 } // 4
let b: Int? = nil
let transformedB = b.flatMap { $0 * 2 } // nil
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]
Третья перегрузка поможет понять, для чего нужен
compactMap
. Эта версия выглядит так же, как и первая, но есть важное отличие. В данном случае замыкание возвращает optional. flatMap
обрабатывает его, пропуская возвращаемые nil-значения, а все остальные — включает в результат в виде значений без обёртки.let array = [1, 2, 3, 4, nil, 5, 6, nil, 7]
let arrayWithoutNils = array.flatMap { $0 } // [1, 2, 3, 4, 5, 6, 7]
Но в этом случае не выполняется упорядочивание. Следовательно, эта версия
flatMap
ближе к map
, чем чисто функциональное определение flatMap
. И проблема с этой перегрузкой заключается в том, что вы можете неправильно использовать её там, где отлично работала бы map
.let array = [1, 2, 3, 4, 5, 6]
let transformed = array.flatMap { $0 } // same as array.map { $0 }
Это применение
flatMap
соответствует третьей перегрузке, неявно обёртывая преобразованное значение в optional, а затем убирая обёртку для добавления в результат. Ситуация становится особенно интересной, если неправильно использовать преобразование строковых значений.struct Person {
let name: String
}
let people = [Person(name: “Foo”), Person(name: “Bar”)]
let names = array.flatMap { $0.name }
В Swift до версии 4.0 мы бы получили преобразование в
[“Foo”, “Bar”]
. Но начиная с версии 4.0 строковые значения реализуют протокол Collection. Следовательно, наше применение flatMap
в данном случае вместо третьей перегрузки будет соответствовать первой, и мы получим «сплющенный» результат из преобразованных значений: [“F”, “o”, “o”, “B”, “a”, “r”]
При вызове
flatMap
вы не получите ошибку, потому что это разрешённое использование. Но логика окажется нарушенной, поскольку результат относится к типу Array<Character>.Type
, а не к ожидаемому Array<String>.Type
.Заключение
Чтобы избежать неправильного использования
flatMap
, из новой версии Swift убрана третья перегруженная версия. А для решения той же задачи (удаления nil-значений) теперь нужно использовать отдельный метод — compactMap
. Комментарии (5)
ManWithBear
24.06.2018 16:31+2Для второй перегрузки выбран не лучший пример: он не показывает разницы между map и flatMap
Более хороший вариант (внимание на вложенность оптионалов):
func magic(_ a: Int) -> Int? { ... } let a: Int? = 1 let mapA = a.map { magic($0) } // Int?? let flatMapA = a.flatMap { magic($0) } // Int?
DnV
Казалось бы, чего стоило заодно и вторую перегрузку переименовать, но, видимо, пока нет конфликтов на логику пофиг.
mayorovp
А с ней-то что не так?
ftp27
В том, что вторая перегрузка тоже разрешает проблему optional типов как и бывшая третья.
alzimin Автор
Тут дело не сколько в опциональности, сколько в неверном использовании flatMap. Многие люди использовали flatMap в контексте, где стоило бы использовать map, а из-за обобщенности протокола Collection такое поведение еще и могло поменяться в Swift 4.0 (а возможно и еще в будущем).
А вот вторая перегрузка вряд ли сможет столкнуться с такой проблемой.