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

в 15:01, , рубрики: flatmap, iOS, swift, Блог компании Badoo, мобильная разработка, Программирование, разработка мобильных приложений, разработка под iOS

Привет!

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

Swift 4.1: почему Apple переименовала flatMap в compactMap - 1

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

Swift 4.1: почему Apple переименовала flatMap в compactMap - 2

Уже на протяжении нескольких версий в 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.

Автор: alzimin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js