Swift 4.1: why Apple renamed flatMap to compactMap
- 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
For several versions Swift has
At first it may not be so easy to understand the essence of innovation. If
The standard Swift library up to version 4.1 provided three overload implementations for
Let's go through all three options and see what they do.
The first overload is for sequences in which the closure takes an element of this sequence and converts it into another sequence.
This is a great example of how a method should work
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.
The third overload will help you understand what you need
But in this case, the ordering is not performed. Therefore, this version is
This application
In Swift to version 4.0, we would get a conversion to
You
To avoid misuse
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 flatMap
takes 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
map
and flatMap
. However, in Swift 4.1, you can no longer apply flatMap
to 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
flatMap
it 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.
flatMap
reduces 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 flatMap
final 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. flatMap
processes 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
flatMap
closer to map
than 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
flatMap
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 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 flatMap
in 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
flatMap
will 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
.