Хабрахабр

[Перевод] Swift 4.1: почему Apple переименовала flatMap в compactMap

Привет, Хабр!

Это перевод статьи моего коллеги Швиба, в которой он рассказал, что из себя представляла функция flatMap в Swift и почему одну из её перегрузок переименовали в compactMap. Меня зовут Александр Зимин, я iOS-разработчик в Badoo. Статья полезна как для понимания процессов, происходящих в репозитории Swift и его эволюции, так и для общего развития.

Метод flatMap берёт список и преобразующую функцию (которая для каждого преобразования ожидает получить ноль или больше значений), применяет её к каждому элементу списка и создаёт единый (flattened) список. В функциональном программировании есть чёткое определение того, что должна представлять собой функция flatMap. Такое поведение отличается от простой функции map, которая применяет преобразование к каждому значению и для каждого преобразования ожидает получить только одно значение.

Однако в Swift 4. Уже на протяжении нескольких версий в Swift есть map и flatMap. Для этого теперь есть метод compactMap. 1 вы больше не можете применять flatMap к последовательности значений и при этом передавать замыкание, которое возвращает опциональное значение.

Если flatMap хорошо работал, зачем вводить отдельный метод? Поначалу может быть не так просто понять суть нововведения. Давайте разберёмся.

1 предоставляла три реализации перегрузки (overloads) для flatMap: Стандартная библиотека Swift до версии 4.

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 // [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”]

Но логика окажется нарушенной, поскольку результат относится к типу Array<Character>. При вызове flatMap вы не получите ошибку, потому что это разрешённое использование. Type. Type, а не к ожидаемому Array<String>.

Заключение

Чтобы избежать неправильного использования flatMap, из новой версии Swift убрана третья перегруженная версия. А для решения той же задачи (удаления nil-значений) теперь нужно использовать отдельный метод — compactMap.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть