company_banner

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

https://badootech.badoo.com/swift-4-1-introduction-of-compactmap-ac4894aad389
  • Перевод
Привет, Хабр!

Меня зовут Александр Зимин, я 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.
  • +31
  • 8,8k
  • 6
Badoo 391,27
Big Dating
Поделиться публикацией
Комментарии 6
  • 0
    Казалось бы, чего стоило заодно и вторую перегрузку переименовать, но, видимо, пока нет конфликтов на логику пофиг.
    • 0
      А с ней-то что не так?
      • 0
        В том, что вторая перегрузка тоже разрешает проблему optional типов как и бывшая третья.
        • 0
          Тут дело не сколько в опциональности, сколько в неверном использовании flatMap. Многие люди использовали flatMap в контексте, где стоило бы использовать map, а из-за обобщенности протокола Collection такое поведение еще и могло поменяться в Swift 4.0 (а возможно и еще в будущем).
          А вот вторая перегрузка вряд ли сможет столкнуться с такой проблемой.
          • +1
            Если помнить что Optional по сути монада, то вторая и первая «перегрузка» идентичны.
            (map каждого элемента в коллекцию с последующим объединением всех коллекций)
      • +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?
        

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

        Самое читаемое