Swift 4.1: why Apple renamed flatMap to compactMap

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 the flatMap function flatMap . The flatMap method takes a list and a conversion 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 map function, which applies a transformation to each value and expects to receive only one value for each conversion.



Already for several versions in Swift there are map and flatMap . However, in Swift 4.1, you can no longer apply flatMap to a sequence of values ​​and at the same time pass a closure that returns an optional value. There is now a compactMap method for compactMap .

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

The Swift standard library up to version 4.1 provided three flatMap overload 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.
flatMap all these transformed sequences into 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 the flatMap method should work. We transform (map) each element of the original list and create a new sequence. Thanks to flatMap end result 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? = 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]


The third overload will help you understand what compactMap . This version looks the same as the first, but there is an important difference. In this case, the closure returns an optional. flatMap processes it by passing the returned nil values, and includes all the rest 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 of flatMap closer to the map than the purely functional definition of flatMap . And the problem with this overload is that you can misuse it where the map would work fine.

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

This flatMap application corresponds 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 transformation 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. Therefore, our use of flatMap in this case instead of the third overload will correspond to the first, and we will get a “flattened” result from the transformed values: [“F”, “o”, “o”, “B”, “a”, “r”]

When you call flatMap you will not get an error, because it is an allowed use. But the logic will be broken, because the result is of type Array<Character>.Type , and not of the expected Array<String>.Type .

Conclusion


To avoid improper use of 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 .

Source: https://habr.com/ru/post/414809/


All Articles