Swift 4.1: why Apple renamed flatMap to compactMap

Original author: Muhammad Shuaib Khan
  • Transfer
Hi, Habr!

My name is Alexander Zimin, I am an iOS developer at Badoo. This is a translation of an article by my colleague Schwib, in which he explained what the flatMap function in Swift was and why one of its overloads was renamed compactMap. The article is useful both for understanding the processes occurring in the Swift repository and its evolution , as well as for general development.



In functional programming, there is a clear definition of what a function should be flatMap. The method flatMaptakes a list and a transformation function (which expects to receive zero or more values ​​for each transformation), applies it to each element of the list and creates a flattened list. This behavior is different from the simple function.map, which applies a transformation to each value and expects to receive only one value for each conversion.



For several versions Swift has mapand flatMap. However, in Swift 4.1, you can no longer apply flatMapto a sequence of values ​​and still pass a closure, which returns an optional value. For this, there is now a method compactMap.

At first it may not be so easy to understand the essence of innovation. If flatMapit worked well, why introduce a separate method? Let's figure it out.

The standard Swift library up to version 4.1 provided three overload implementations for 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]

Let's go through all three options and see what they do.

Sequence.flatMap <S> (_: (Element) -> S) -> [S.Element], where S: Sequence


The first overload is for sequences in which the closure takes an element of this sequence and converts it into another sequence.
flatMapreduces all these converted sequences to the final sequence returned as a result. For example:

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]

This is a great example of how a method should work flatMap. We transform (map) each element of the original list and create a new sequence. Due to the flatMapfinal result, it is a flattened structure of transformed sequences.

Optional.flatMap <U> (_: (Wrapped) -> U?) -> U?


The second overload is for optional types. If the optional type you are calling has a value, then the closure will be called with a value without an optional wrapper (unwrapped value), and you can return the converted optional value.

let a: Int? = 2let transformedA = a.flatMap { $0 * 2 } // 4let b: Int? = nil
let transformedB = b.flatMap { $0 * 2 } // nil

Sequence.flatMap <U> (_: (Element) -> U?) -> [U]


The third overload will help you understand what you need compactMap. This version looks the same as the first, but there is an important difference. In this case, the closure returns an optional. flatMapprocesses it by passing the returned nil-values, and includes all others in the result as values ​​without a wrapper.

let array = [1, 2, 3, 4, nil, 5, 6, nil, 7]
let arrayWithoutNils = array.flatMap { $0 } // [1, 2, 3, 4, 5, 6, 7]

But in this case, the ordering is not performed. Therefore, this version is flatMapcloser to mapthan a purely functional definition flatMap. And the problem with this overload is that you can misuse it where you’d work perfectly map.

let array = [1, 2, 3, 4, 5, 6]
let transformed = array.flatMap { $0 } // same as array.map { $0 }

This application flatMapcorresponds to the third overload, implicitly wrapping the transformed value in optional, and then removing the wrapper to add to the result. The situation becomes especially interesting if you incorrectly use the conversion of string values.

struct Person {
    let name: String
}
let people = [Person(name: “Foo”), Person(name: “Bar”)]
let names = array.flatMap { $0.name }

In Swift to version 4.0, we would get a conversion to [“Foo”, “Bar”]. But since version 4.0, string values ​​implement the Collection protocol. Consequently, our application flatMapin this case, instead of the third overload, will correspond to the first one, and we will get a “flattened” result from the converted values: [“F”, “o”, “o”, “B”, “a”, “r”]

You flatMapwill not get an error in the call because it is a permitted use. But the logic will be broken, because the result refers to the type Array<Character>.Type, not to the expected Array<String>.Type.

Conclusion


To avoid misuse flatMap, the third overloaded version is removed from the new version of Swift. And to solve the same problem (delete nil-values), now you need to use a separate method - compactMap.

Also popular now: