Swift: map vs. flatMap For Dummies
tl;dr Summary #
This is me, figuring out how map
and flatMap
work in Swift.
tl;dr Conclusions: #
- Think of optionals as an array that can hold 1 or 0 items.
- Arrays and optionals are both containers that can have
map
orflatMap
applied map
unwraps a container, transforms value with supplied function and rewraps result.flatMap
unwraps a container, transforms value with supplied function and rewraps either a raw value, or the non-nil value of a contained result[confusing]
Complete Version With Complete Examples #
Recently, I had an issue where I was using a tableView with multiple selection allowed, and I had a short array with names ([String]
), and I wanted a map
function that would return the index of that name in a larger array with all the possible names.
smallNameArray.map({ largeNameArray.indexOf($0)! })
However, .indexOf()
returns an optional value, and Swift won’t let map return nil values. I probably could have written a longer function that used if let
somehow. But then I remembered something about flatMap
being helpful in such cases. A [little][RussLink] [reading][MonadDo] [later][MonadsInPictures], I had a better understanding of both map
and flatMap
, and my short version is that flatMap
will take an array of optionals or an array of arrays and return an array of the raw values contained in those optionals or inner arrays. In other words, it “flattens” the containers by one level.
While there have already been countless posts, on this, they were not quite as systematic with all cases as this “dummy” needed, so let me lay this out further.
First, in addition to “raw” types, we have container types[1]. I will focus on just two containers, Array
and Optional
. The key point for today is that containers can have something in them or they can have nothing in them.
Array
: [String]
[typeNote], the brackets represent an array container around an unknown number of values, ranging from 0 to n.
Optional
: String?
, the question mark represents a container around an unknown number of values, ranging from 0 to 1. If it is empty, we call that nil
[opt]. It can be helpful to think of an optional as an array with a maximum capacity of 1.
container type | filled state | empty state |
---|---|---|
array | [“one”, “two”] | [] |
optional | Opt(“one”) | Opt(nil) i.e. nil [optReminder] |
Second, a function fundamentally(at least for the sake of this conversation) accepts and produces either of these two types as input/output: It can accept/produce raw types (Int
, Float
, String
, etc.) or it can accept/produce contained types (Optional
, Array
, etc.).
So then we can understand map
vs. flatMap
in the following way:
Map #
- Unwraps a container, which is either empty or filled.
- If something (or multiple things) of the correct type is (are) there, it will apply the function to it (or each of them).
- It then puts that complete result back into the original container. Because the function can return either a raw type OR a container type, this can result in a container in the original container when the return type is a container type.
FlatMap #
- Unwraps a container, which is either empty or filled. [2]
- If something (or multiple things) is (are) there, it will apply the function to each of them.
- If it is a raw type, it will put the result(s) back into the original container as is. If the result from the function is a container type, it will put the non-empty contents of the contained result(s) back into the original container.
So, in an effort to wrap my head around this and get a complete picture I wrote a set of functions that I posted in an [IBM’s sandbox][ibmSandbox]. As [Natasha the Robot][NatashaLink] posted, it is helpful to see the results of map/flatMap on Array
and Optional
side by side, because it helps make clear the role that optional plays as a container.
After struggling with the best way to show the results, I have made a series of tables with Optional
and Array
treatments side-by-side. You can look at the functions in the cited sandbox if you want to play with them further.
Apply String -> String
Function To A Filled Container #
Opt(“A String”), [“A String”]
container type | optional version | array version |
---|---|---|
function | String -> String | String -> [String] |
string in a container | Opt(“A String”) | [“A String”] |
container.map(func) | Opt(“A String”) | [“A String”] |
container.flatMap(func) | Opt(“A String”) | [“A String”] |
The inner value of the container is a string. The function accepts a Raw String (because that is what inside of the container) and returns a Raw String.
map
: rewraps the function’s return value (Raw String) inside of the original container.
flatMap
: rewraps the function’s return value (Raw String) into the outer container.
Apply String -> Container
Function To A Filled Container #
Opt(“A String”), [“A String”]
container type | optional version | array version |
---|---|---|
function | String -> String? | String -> [String] |
string in a container | Opt(“A String”) | [“A String”] |
container.map(func) | Opt(Opt(“A String”)) | [[“A String”]] |
container.flatMap(func) | Opt(“A String”) | [“A String”] |
The inner value of the container is a string. The function accepts a Raw String (because that is what inside of the container) and returns a contained string (String? or [String]).
map
: rewraps the function’s return value (contained String) inside of the original container.
flatMap
: places non-nil result of the function’s return value (String) into the outer container.
Apply String -> Container
Function To An Empty Container #
Opt(nil), []
container type | optional version | array version |
---|---|---|
function | String -> String? | String -> [String] |
empty container | Opt(nil) | [] |
container.map(func) | Opt(Opt(nil)) | [[]] |
container.flatMap(func) | (Opt(nil) | [] |
The inner value of the container is a string. The function accepts a Raw String (because that is what inside of the container) and returns a contained string (String? or [String]).
map
: rewraps the function’s return value (contained String) inside of the original container.
flatMap
: places non-nil result of the function’s return value (String) into the outer container.
Apply Container -> Container
Function 1/3: Filled Inner Container #
To keep things simple, I have broken the “double container” examples into three examples.
Opt(Opt(“A String”)), [[“A String”]]
container type | optional version | array version |
---|---|---|
function | String? -> String? | [String] -> [String] |
container w/ string in container | Opt(Opt(“A String”)) | [[“A String”]] |
doubContainer.map(func) | Opt(Opt(“A String”)) | [[“A String”]] |
doubContainer.flatMap(func) | Opt(“A String”) | [“A String”] |
The inner value of the container is a string. The function accepts a Raw String (because that is what inside of the container) and returns a contained string (String? or [String]).
map
: rewraps the function’s return value (contained String) inside of the original container.
flatMap
: places non-nil result of the function’s return value (String) into the outer container.
Apply Container -> Container
Function 2/3: Empty Inner Container #
Opt(Opt(nil)), [[]]
container type | optional version[optReminder] | array version |
---|---|---|
function | String? -> String? | [String] -> [String] |
empty container in container | Opt(Opt(nil)) | [[]] |
doubContainer.map(func) | Opt(Opt(nil)) | [[]] |
doubContainer.flatMap(func) | Opt(nil) | [] |
The inner value of the container is empty. The function accepts a contained Raw String, which happens to be empty, and returns an empty container: ( Opt(nil) or [] ).
map
: rewraps the function’s return value (empty container) inside of the original container.
flatMap
: places nothing into the outer container, because there was no non-nil result, so there is a single depth, empty container
Apply Container -> Container
Function 3/3: Array With Multiple Containers #
[Opt(“A String”), Opt(nil)], [[“A String”], []]
container type | optional version | array version |
---|---|---|
function | String? -> String? | [String] -> [String] |
array of containers in container | [Opt(“A string”), Opt(nil)] | [[“A String”, []] |
doubContainer.map(func) | [Opt(“A String”), Opt(nil) | [[“A string”], []] |
doubContainer.flatMap(func) | [“A string”] | [“A string”] |
This example simply combines the previous two examples and makes explicit what happens to an array that is filled with containers that are both empty and filled. Since optionals cannot hold more than one value, I used an array as the outer container for both examples, and put two containers in the array, one that is empty and one that is filled.
As, expected, map
rewraps the results inside of the array, resulting in an array of contained strings and nils with a count of 2. flatMap
places only the non-nil contained value into the array, resulting in an array of strings with a count of one, because there was only one non-nil result.
I hope this is helpful to someone. It really helped me a lot. I almost feel like I have an intuition for this now.
[ibmSandbox]: http://swiftlang.ng.bluemix.net/#/repl/fd37e9c672a7e365c64b82bbd6eb8b97b8d9f7e875f3a4628b9a60b05afccf57
[MonadDo]: http://sketchytech.blogspot.com/2015/06/swift-what-do-map-and-flatmap-really-do.html
[MonadsInPictures]: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
[RussLink]: http://www.russbishop.net/monoids-monads-and-functors
[NatashaLink]: https://www.natashatherobot.com/swift-2-flatmap/
[opt]: Swift prints an optional with nothing inside of it as “nil” (I finally looked at the code for debugDescription of ?
in Open Source Swift, not Opt(nil). This caused me a lot of confusion for a while! In fact, it now occurs to me that in Swift, the definition of nil, is essentially an empty optional container.
[optReminder]: when printing/logging an empty optional, Opt(nil)
, it prints as nil
[1]: Monads, but that is a stupid word and I won’t use it.
[2]: this
[typeNote]: (String could be any type. I am just using it here and throughout as the example “raw” type)
[confusing]: Confusing? Read more and see examples