Swift: Further adventures in flatMap()


After my post yesterday, I looked around the web a bit to see if I could find posts on flatMap() that describe the behaviour in clear terms. I didn't find what I was looking for but I did find plenty on Scala. One or two posts in particular struck me in terms of clarity and helped me to experiment some more in Swift, while others threw me back into the swamp with the monads (and double monads!).

The results of these experiments are presented here.

Strings

If a string is thrown into the map() function what is returned is an array of Characters, but if it is passed to flatMap() then a string is returned.
var str = "Hello, playground"

let mapString = map(str){$0}
mapString // ["H", "e", "l", "l", "o", ",", " ", "p", "l", "a", "y"]
let flatMapString = flatMap(str){$0}
flatMapString // "Hello, playground"
However, if the property .uppercaseString is added to each element then map() and flatMap() return identical results:
var str = "Hello, playground"

let mapString = map(str){$0.uppercaseString}
mapString // "HELLO, PLAYGROUND"
let flatMapString = flatMap(str){$0.uppercaseString}
flatMapString // "HELLO, PLAYGROUND"
While attempting to use mutating methods like append() and extend() on the elements of the string raise errors, a method such as stringByAppendingString() sees flatMap() and map() once again behave identically.
var str = "Hello, playground"
let mapString = map(str){$0.stringByAppendingString("s")}
mapString // "Hello, playgrounds"
let flatMapString = flatMap(str){$0.stringByAppendingString("s")}
flatMapString // "Hello, playgrounds"
Similarly, if we count the elements then the same result is returned for map() and flatMap():
var str = "Hello, playground"

let mapString = map(str){count($0)}
mapString // 17
let flatMapString = flatMap(str){count($0)}
flatMapString // 17
But as soon as we place the string within an array the behaviour changes:
var str = ["Hello, playground"]

let mapString = map(str){count($0)}
mapString // [17]
let flatMapString = flatMap(str){count($0)}
flatMapString // 1
Because flatMap() is now looking at the array and performing a count of its items, not the characters within each of its items.
let flatMapString = flatMap(str){$0.map{count($0)}}
flatMapString // [17]

Optional strings

Some interesting behaviour surrounds the mapping of optional strings. When they stand alone then map() and flatMap() see them equally and count the characters within them:
var str:String? = "Hello, playground"

let mapString = map(str){count($0)}
mapString // 17
let flatMapString = flatMap(str){count($0)}
flatMapString // 17
And since it's not optionals being counted but the string itself, a nil value consistently returns nil:
var str:String? = nil

let mapString = map(str){count($0)} // nil
mapString // nil
let flatMapString = flatMap(str){count($0)} // nil
flatMapString // nil
But placed inside an array, map() and flatMap() only count the number of optionals:
var str:[String?] = ["Hello, playground","hi"]

let mapString = map(str){count($0)}
mapString // 2
let flatMapString = flatMap(str){count($0)}
flatMapString // 2
It is only when we add in an additional level of mapping that map() can see inside the optionals, but here the behaviour of flatMap() diverges because it generates an error when we attempt the same thing.
var str:[String?] = ["Hello, playground","hi"]

let mapString = map(str){$0.map{count($0)}}
mapString // [{Some 17}, {Some 2}]
let flatMapString = flatMap(str){$0.map{count($0)}} // error
The reason for this error appears to be because map() is returning [String?] whereas flatMap() is returning [String?]?. This error can be circumvented by force unwrapping:
let flatMapString = flatMap(str){$0.map(){count($0!)}}
But in fact the better option is to have flatMap return an array [String?] by doing the following:
flatMap(str){[$0].map{count($0)}}
This makes it much easier to handle the case where nil is contained in optional String array [String?], because the force unwrap approach would simply crash. Although the rule really should be that you don't use flatMap() to do the job of map(). The flatMap() function doesn't need to get involved in  one-dimensional activities, since there is nothing to flatten. And while it can, it shouldn't.

Thanks go to Al Skipp for these latter insights.

Tuples

There's more behaviour that can be looked at, and I could test and test, but I wanted to end on this finding.
let tuple = [("hello","hello"),("goodbye","goodbye")]

let mapTuple = map(tuple){count($0)}
mapTuple // 2
let flatMapTuple = flatMap(tuple){count($0)}
flatMapTuple // 2
As before we see map() and flatMap() in a given situation behaving identically, but more interesting to me is that we can map() and map() tuples endlessly and unlike optionals, which surrender their inner contents, tuples are a hard nut that are seemingly impossible to crack:
let tuple = [("hello","hello"),("goodbye","goodbye")]

let mapTuple = map(tuple){$0.map{$0}.map{$0}.map{$0}}
mapTuple // [(.0 "hello", .1 "hello"), (.0 "goodbye", .1 "goodbye")]
let flatMapTuple = flatMap(tuple){$0.map{$0}.map{$0}.map{$0}}
flatMapTuple // [(.0 "hello", .1 "hello"), (.0 "goodbye", .1 "goodbye")]
And yet this is at the same time an illusion because if we tried to count the results of the secondary maps, an error would be returned.

Conclusion

While I understand the way in which flatMap() handles multidimensional arrays (explained at the beginning of yesterday's post), the handling of more complex scenarios is currently eluding me. I will therefore keep reading and researching to try and discover more. And if I arrive at a clear understanding of the behaviour, I will of course report back.

In the meantime, I urge those with a more sophisticated understanding to write clearly and cleanly about the topic for those of us still learning. Write code that doesn't add additional boxing and so forth, explain without leaning on functional terms with slippery (and difficult to explain) meanings. And leave out the custom operators and musings over ==- or <- and so on. Most of all we need one thing explained at a time. At least I do!


Endorse on Coderwall

Comments