Swift 2.0: Understanding flatMap
Last week I wrote a blog post about a problem I was trying to solve – trying to create an array of non-optional sequenced images.
While trying to come up with the best solution, it did pop into my head to consider flatMap
, but to be honest, I didn’t know much about flatMap
or how to use it. A co-worker sent me a solution using double flatMap
, and it didn’t look very readable or appealing.
After a great discussion in the comments and on Twitter, I learned that the solution was in fact a very simple use of flatMap:
1 |
let minionImagesFlattened = (1...7).flatMap { UIImage(named: "minionIcon-\($0)") } |
So my goal here is to summarize the great discussion about flatMap
into how I now understand it. Keep in mind that I’m learning here, so I’m definitely not the expert on it!
The Simple Use-Case
My understanding of flatMap
was very basic. Here is how I initially thought of it:
1 2 3 4 |
let nestedArray = [[1,2,3], [4,5,6]] let flattenedArray = nestedArray.flatMap { $0 } flattenedArray // [1, 2, 3, 4, 5, 6] |
Transforming Elements aka 😡
As I was writing the example above, I wanted to do something very simple – multiply each element by 2, just like I would with map
. Instead I got this:
No matter what I tried to do in the flatMap block, it didn’t work 😢. So I googled around, and thankfully came across a blog post I’ve seen before but should have read a lot more carefully: What do map() and flatMap() really do? by @sketchyTech. Go ahead and read it. So much useful stuff there about flatMap
!
The part I’m going to point out as the biggest (and now very obvious) takeaway, is that the $0
inside of the flatMap
refers to one of the arrays inside the array of arrays! Duh! So if you want to multiply the elements inside the array, you have to go one level deeper and use map:
1 2 3 4 |
let nestedArray = [[1,2,3], [4,5,6]] let multipliedFlattenedArray = nestedArray.flatMap { $0.map { $0 * 2 } } multipliedFlattenedArray // [2, 4, 6, 8, 10, 12] |
Here is the fully written-out version with names instead of the shortcut $0 in case that makes it easier to understand:
1 2 3 4 5 6 7 |
let nestedArray = [[1,2,3], [4,5,6]] let multipliedFlattenedArray = nestedArray.flatMap { array in array.map { element in element * 2 } } multipliedFlattenedArray // [2, 4, 6, 8, 10, 12] |
flatMap + Optionals 😕
Since I was thinking of flatMap
as dealing with nested arrays, I had a very hard time making the mental leap to understanding how it was used in my original problem:
1 |
let minionImagesFlattened = (1...7).flatMap { UIImage(named: "minionIcon-\($0)") } |
But apparently, flatMap
has magic powers when it comes to optionals:
@NatashaTheRobot the new flatMap is basically “map, but strip out nil values”. IOW, it returns [T], not [T?].
— Dave DeLong (@davedelong) July 25, 2015
Sure enough, when I looked at the function definition for flatMap, I found this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
extension SequenceType { /// Return an `Array` containing concatenated results of mapping `transform` /// over `self`. func flatMap<S : SequenceType>(@noescape transform: (Self.Generator.Element) -> S) -> [S.Generator.Element] } extension SequenceType { /// Return an `Array` containing the non-nil results of mapping `transform` /// over `self`. func flatMap<T>(@noescape transform: (Self.Generator.Element) -> T?) -> [T] } |
In other words, flatMap
is specifically overloaded to deal with Optionals. It will take an array of optionals and return an array of unwrapped optionals without any nils.
1 2 3 4 |
let optionalInts: [Int?] = [1, 2, nil, 4, nil, 5] let ints = optionalInts.flatMap { $0 } ints // [1, 2, 4, 5] - this is an [Int] |
Very nice and convenient! But how does this relate to the example above of flattening nested arrays? Whey was flatMap
overloaded to do this? The best explanation I’ve seen comes from this comment from Lars-Jørgen Kristiansen:
The idea of thinking of flatMap
as dealing with containers vs just arrays makes things a lot clearer!
I’m sure I’m only covering the tip of the iceberg here when it comes to flatMap, but I feel like I at least have a starting point to work with here. Happy Swift 2.0!