Swift: map vs. flatMap For Dummies

tl;dr Summary #

This is me, figuring out how map and flatMap work in Swift.

tl;dr Conclusions: #

  1. Think of optionals as an array that can hold 1 or 0 items.
  2. Arrays and optionals are both containers that can have map or flatMap applied
  3. map unwraps a container, transforms value with supplied function and rewraps result.
  4. 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 #

  1. Unwraps a container, which is either empty or filled.
  2. If something (or multiple things) of the correct type is (are) there, it will apply the function to it (or each of them).
  3. 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 #

  1. Unwraps a container, which is either empty or filled. [2]
  2. If something (or multiple things) is (are) there, it will apply the function to each of them.
  3. 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

 
34
Kudos
 
34
Kudos

Now read this

Random Reminder of the Day: UISwitch isOn vs. on – Object.property Is Not a Variable, It Is a Method.

I know that I should stop talking about objective-C. sorry. It is interesting to see how coming into the Objective-C language in the last couple of years blinds you to some of the more traditional uses of the language. In particular the... Continue →