Swift: What do map() and flatMap() really do?


map()

Let's start with map() and feed it an array:
let arr = [1,2,3,4,5,6]
let mapArr = map(arr){$0 * 2}
mapArr // [2, 4, 6, 8, 10, 12]
For those used to closures (and generics), the logic here will be straightforward. We feed map() an array, it works its way through the elements and does stuff to them in order to return a new array. Not only is this true, but the new array that is generated doesn't need to be of the same type as the original array:
let arr = [1,2,3,4,5,6]
let mapArr = map(arr){Double($0) * 2.5}
mapArr // [2.5, 5, 7.5, 10, 12.5, 15]
And so we could go on:
let arr = [1,2,3,4,5,6]
let mapArr = map(arr){$0 > 2}
mapArr // [false, false, true, true, true, true]
and on:
let arr = [1,2,3,4,5,6]
let mapArr = map(arr){$0 > 2 ? $0 as Int? : nil}
mapArr // [nil, nil, {Some 3}, {Some 4}, {Some 5}, {Some 6}]

flatMap() 

Now let's start doing the same thing with flatMap()
let arr = [1,2,3,4,5,6]
let flatMapArr = flatMap(arr){$0 * 2} // error cannot find an overload, blah blah blah
We get an error and no matter how we try to manipulate the contents of the array - divide, add, etc. - the bloody error won't go away.

Why is this happening to me, arrghh!

It's happening because flatMap doesn't operate on the contents of the array, it operates on the array itself. So if we were to manipulate the $0 element using Array methods then we'd have more luck:
let arr = [1,2,3,4,5,6]
let flatMapArr = flatMap(arr){$0.count}
flatMapArr // 6
Marvellous, we can do what we'd be able to do to the array without ever using flatMap! That's not all we could do this as well:
let arr = [1,2,3,4,5,6]
let flatMapArr = flatMap(arr){$0.map{$0 * 2}}
flatMapArr // [2, 4, 6, 8, 10, 12]
Which is just as pointless because we could've just used map() on the array.

Show me something useful

Let's suppose we weren't handling an array of Int but an array of arrays of Int:
let arr = [[1,2,3],[4,5,6]]
let mapArr = map(arr){$0 * 2} // cannot find an overload, blah blah blah
Now look who's got the error, it's our dear and easy to understand friend map(). Well OK, but think about this the arrays are now the elements being mapped, so if we want to change the Int values then we're going to need to map the elements of the elements we're mapping, like so:
let arr = [[1,2,3],[4,5,6]]
let mapArr = map(arr){$0.map{$0 * 2}}
mapArr // [[2, 4, 6], [8, 10, 12]]
Now for the interesting bit: flatMap() sees the world differently to map(), flatMap() lives in flatland. When it looks at an array of arrays [[1,2,3],[4,5,6]] what it sees is [1,2,3,4,5,6].
let arr = [[1,2,3],[4,5,6]]
let flatMapArr = flatMap(arr){$0}
flatMapArr // [1, 2, 3, 4, 5, 6]
So if we were to return to our attempts to multiply values by two
let arr = [[1,2,3],[4,5,6]]
let flatMapArr = flatMap(arr){$0.map{$0 * 2}}
flatMapArr // [2, 4, 6, 8, 10, 12]
our resulting array would be one-dimensional not two-dimensional, as with map().

Tell me more about the logic

The flatMap() function doesn't see the world as entirely flat, it just sees one less dimension than map() and the rest of the Swift world. Therefore, [[[1],[2,3]],[[4,5],[6]]] would flatMap() to [[1], [2, 3], [4, 5], [6]], for example.
let arr = [[[1],[2,3]],[[4,5],[6]]]
let flatMapArr = flatMap(arr){$0}
flatMapArr // [[1], [2, 3], [4, 5], [6]]
To explain: one-dimensional and two-dimensional arrays map to one-dimensional arrays, three-dimensional to two-dimensional and so on. But this is a little misleading, because actually when we feed flatMap() something like [["one"],["three","four"]], it will see two arrays and that's what it'll count:
let arr = [["one"],["three","four"]]
let mapArr = flatMap(arr){count($0)}
mapArr // 2
Whereas if we fed it ["one","three","four"]
let arr = ["one","three","four"]
let mapArr = flatMap(arr){count($0)}
mapArr // 3
the count would be three. But if we were to map the inner contents of those arrays then they would both be treated the same and return the same result using the same flatMap() and map() combo. See here:
let arr = [["one"],["three","four"]]
let mapArr = flatMap(arr){$0.map{count($0)}}
mapArr // [3, 5, 4]
And here:
let arr = ["one","three","four"]
let mapArr = flatMap(arr){$0.map{count($0)}}
mapArr // [3, 5, 4]
However, once buried deeper than the second dimension, the nested behaviour is reflected in the results:
let arr = [[["one"]],[["three","four"]]]
let mapArr = flatMap(arr){$0.map{count($0)}}
mapArr // [1, 2]
We see here that what's been counted are the number of inner arrays not the (string) elements within those arrays, as was the case before. This is in some ways a strange logic and things get stranger when dealing with dictionaries because an array of dictionaries for example cannot simply be flattened (in case key values are repeated?), so one- and two-dimensional dictionaries do not flatMap() to the same result (as arrays do). As can be seen here:
let dict = [["one":1],["three":3,"four":4]]
let mapDict = flatMap(dict){$0}
mapDict // [["one": 1], ["three": 3, "four": 4]]
And here:
let dict = ["one":1,"three":3,"four":4]
let mapDict = flatMap(dict){$0}
mapDict // ["one": 1, "three": 3, "four": 4]
And whereas when fed a one-dimensional array, flatMap() would see only the whole array, when fed a dictionary it follows the same behaviour as map(). This means that this
let dict = ["one":1,"three":3,"four":4]
let mapDict = flatMap(dict){$0["one"]}
mapDict // 1
is equivalent to this:
let dict = ["one":1,"three":3,"four":4]
let mapDict = map(dict){$0["one"]}
mapDict // 1
However, the secondary use of flatMap(), when dealing with nested dictionaries:
let dict = [["one":1],["three":3,"four":4]]
let mapDict = flatMap(dict){$0.flatMap{$0["one"]}} // error
is not equivalent to:
let dict = [["one":1],["three":3,"four":4]]
let mapDict = flatMap(dict){$0.map{$0["one"]}}
mapDict // [{Some 1}, nil]
And this makes the behaviour of map() and flatMap() difficult to understand and apply a blanket rule to. This is because I can list what happens and see that one-dimensional dictionaries and arrays have special rules, but I can't explain why. Not only this but what happens to the logic of flatMap() when I have increasingly complex types? For example, a dictionary with values that are dictionaries. At the moment all I seem to get is Playground freezes in Xcode as I experiment with these things. (At this point I invite comments from those who can clarify these differences in behaviour.)

Where is the practical use in this?

I have to confess that I'm not a fully-signed up functional thinking programmer, and that I'm still in transition, purgatory, a functional-curious state, call it what you will. What I can see is that flatMap() saves us some drilling time when we want to extract values in multi-dimensional arrays. I expect it also provides benefits when approaching tasks in a functional way.

The first hurdle for me, however, was to find out how to use flatMap() and how to stop simply generating errors. Often functional explanations can appear to the uninitiated like a series of smoke and mirrors with talk of functors, monads, binds and custom operators creating a brain fog. This is no fault of the those explaining, they already see clearly and think clearly within the functional paradigm, and it's very hard for people explaining to remember exactly the small bits and pieces that are essential to understanding the whole. And why those bits and pieces are difficult to understand. But the result is that the uninitiated (like myself) end up reading article after article in an attempt to arrive at a clear understanding of certain fundamental functions. And time and again the fog descends.

To read more on this subject see Swift: Further adventures in flatMap().

Endorse on Coderwall

Comments

  1. syntax changed as below :

    let arr = [1,2,3,4,5,6]
    let mapArr = arr.map {Double($0) * 2.5}
    print(mapArr)

    likewise you can maintain all.

    ReplyDelete

Post a Comment