Swift 3: Map and FlatMap Demystified
Posted on October 8, 2015 中文版
Update 12/16:This post has been verified with Swift 3, minimal changes were required.
Get this and other playgrounds from GitHub or zipped.
Swift is a language still slightly in flux, with new functionalities and alterations of behavior being introduced in every release. Much has already been written about the functional aspects of Swift and how to approach problems following a more “pure” functional approach.
Considering that the language is still in its infancy, often, trying to understand some specific topics you’ll end up reading a lot of articles referring to old releases of the language, or worst, descriptions that mix up different releases. Sometimes, searching for articles on flatMap
, you could even fortuitously find more than one really good articles explaining Monads in the context of Swift.
Add to the lack of comprehensive and recent material the fact that many of these concepts, even with examples or daring metaphors, are not obvious, especially for someone used to the imperative way of thinking.
With this short article (part of a series on Swift and the functional approach) I’ll try to give a clear and throughout explanation of how map
and especially flatMap
work for different types, with references to the current library headers.
Contents
Map
Map has the more obvious behavior of the two *map functions, it simply performs a closure on the input and, like flatMap
, it can be applied to Optionals and Sequences (i.e. arrays, dictionaries, etc..).
Map on Optionals
For Optionals, the map function has the following prototype:
public enum Optional<Wrapped> : ... {
...
/*
- Parameter transform: A closure that takes the unwrapped value
of the instance.
- Returns: The result of the given closure. If this instance is `nil`,
returns `nil`.
*/
public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
...
}
The map function expects a closure with signature (Wrapped) -> U
, if the optional has a value applies the function to the unwrapped optional and then wraps the result in an optional to return it (an additional declaration is present for implicitly unwrapped optionals, but this does not introduce any difference in behavior, just be aware of it when map doesn’t actually return an optional).
Note that the output type can be different from the type of the input, that is likely the most useful feature.
Straightforward, this does not need additional explanations, let’s see some real code from the playground for this post:
var o1:Int? = nil
var o1m = o1.map({$0 * 2})
o1m /* Int? with content nil */
o1 = 1
o1m = o1.map({$0 * 2})
o1m /* Int? with content 2 */
var os1m = o1.map({ (value) -> String in
String(value * 2)
})
os1m /* String? with content 2 */
os1m = o1.map({ (value) -> String in
String(value * 2)
}).map({"number "+$0})
os1m /* String? with content "number 2" */
Using map on optionals could save us an if each time we need to modify the original optional (map applies the closure to the content of the optional only if the optional has a value, otherwise it just returns nil), but the most interesting feature we get for free is the ability to concatenate multiple map operations that will be executed sequentially, thanks to the fact that a call to map
always return an optional. Interesting, but quite similar and more verbose than what we could get with optional chaining.
Map on Sequences
But it’s with Sequences
like arrays and dictionaries that the convenience of using map-like functions is hard to miss:
var a1 = [1,2,3,4,5,6]
var a1m = a1.map({$0 * 2})
a1m /* [Int] with content [2, 4, 6, 8, 10, 12] */
let ao1:[Int?] = [1,2,3,4,5,6]
var ao1m = ao1.map({$0! * 2})
ao1m /* [Int] with content [2, 4, 6, 8, 10, 12] */
var a1ms = a1.map({ (value) -> String in
String(value * 2)
}).map { (stringValue) -> Int? in
Int(stringValue)
}
a1ms /* [Int?] with content [.Some(2),.Some(4),.Some(6),.Some(8),.Some(10),.Some(12)] */
This time we are calling the .map function defined on Sequence
as follow:
/*
- Parameter transform: A mapping closure. `transform` accepts an
element of this sequence as its parameter and returns a transformed
value of the same or of a different type.
- Returns: An array containing the transformed elements of this
sequence.
*/
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
The transform closure of type (Element) -> T
is applied to every member of the collection and all the results are then packed in an array with the same type used as output in the closure and returned. As we did in the optionals example, sequential operation can be pipelined invoking map
on the result of a previous map
operation.
This basically sums up what you can do with map
, but before moving to flatMap
, let’s see three additional examples:
var s1:String? = "1"
var i1 = s1.map {
Int($0)
}
i1 /* Int?? with content 1 */
var ar1 = ["1","2","3","a"]
var ar1m = ar1.map {
Int($0)
}
ar1m /* [Int?] with content [.Some(1),.Some(2),.Some(3),nil] */
ar1m = ar1.map {
Int($0)
}
.filter({$0 != nil})
.map {$0! * 2}
ar1m /* [Int?] with content [.Some(2),.Some(4),.Some(6)] */
Not every String can be converted to an Int, so our integer conversion closure will always return an Int?.
What happens in the first example with that Int??, is that we end up with an optional of an optional, for the additional wrapping performed by map. To actually get the contained value will need to unwrap the optional two times, not a big problem, but this starts to get a little inconvenient if we need to chain an additional operation to that map. As we’ll see, flatMap
will help with this.
In the example with the array, if a String cannot be converted as it happens for the 4th element of ar1
the that element in the resulting array will be nil. But again, what if we want to concatenate an additional map operation after this first map and apply the transformation just to the valid (not nil) elements of our array to obtain a shorter array with only numbers?
Well, we’ll just need intermediate filtering to sort out the valid elements and prepare the stream of data to the successive map operations. Wouldn’t it be more convenient if this behavior was embedded in map
?
We’ll see that this another use case for flatMap
.
FlatMap
The differences between map
and flatMap
could appear to be minor but they are definitely not.
While flatMap
is still a map-like operation, it applies an additional step called flatten
right after the mapping phase.
Let’s analyze flatMap
’s behavior with some code like we did in the previous section.
FlatMap on Optionals
The definition of the function is a bit different, but the functionality is similar, as the reworded comment implies:
public enum Optional<Wrapped> : ... {
...
/*
- Parameter transform: A closure that takes the unwrapped value
of the instance.
- Returns: The result of the given closure. If this instance is `nil`,
returns `nil`.
*/
public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
...
}
There is a substantial difference regarding the closure, flatMap
expects a (Wrapped) -> U?)
this time.
With optionals, flatMap applies the closure returning an optional to the content of the input optional and after the result has been “flattened” it’s wrapped in another optional.
Essentially, compared to what map
did, flatMap
also unwraps one layer of optionals.
var fo1:Int? = nil
var fo1m = fo1.flatMap({$0 * 2})
fo1m /* Int? with content nil */
fo1 = 1
fo1m = fo1.flatMap({$0 * 2})
fo1m /* Int? with content 2 */
var fos1m = fo1.flatMap({ (value) -> String? in
String(value * 2)
})
fos1m /* String? with content "2" */
var fs1:String? = "1"
var fi1 = fs1.flatMap {
Int($0)
}
fi1 /* Int? with content "1" */
var fi2 = fs1.flatMap {
Int($0)
}.map {$0*2}
fi2 /* Int? with content "2" */
The last snippet contains and example of chaining, no additional unwrapping is needed using flatMap
.
As we’ll see again when we describe the behavior with Sequences, this is the result of applying the flattening step.
The flatten
operation has the sole function of “unboxing” nested containers. A container can be an array, an optional or any other type capable of containing a value with a container type. Think of an optional containing another optional as we’ve just seen or array containing other array as we’ll see in the next section.
This behavior adheres to what happens with the bind
operation on Monads, to learn more about them, read here and here.
FlatMap on Sequences
Sequence provides the following implementations of flatMap
:
/// - Parameter transform: A closure that accepts an element of this
/// sequence as its argument and returns a sequence or collection.
/// - Returns: The resulting flattened array.
///
public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throws ->: SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element]
/// - Parameter transform: A closure that accepts an element of this
/// sequence as its argument and returns an optional value.
/// - Returns: An array of the non-`nil` results of calling `transform`
/// with each element of the sequence.
///
public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
flatMap
applies those transform closures to each element of the sequence and then pack them in a new array with the same type of the input value.
These two comments blocks describe two functionalities of flatMap
: sequence flattening and nil optionals filtering.
Let’s see what this means:
var fa1 = [1,2,3,4,5,6]
var fa1m = fa1.flatMap({$0 * 2})
fa1m /*[Int] with content [2, 4, 6, 8, 10, 12] */
var fao1:[Int?] = [1,2,3,4,nil,6]
var fao1m = fao1.flatMap({$0})
fao1m /*[Int] with content [1, 2, 3, 4, 6] */
var fa2 = [[1,2],[3],[4,5,6]]
var fa2m = fa2.flatMap({$0})
fa2m /*[Int] with content [1, 2, 3, 4, 6] */
While the result of the first example doesn’t differ from what we obtained using map
, it’s clear that the next two snippets show something that could have useful practical uses, saving us the need for convoluted manual flattening or filtering.
In the real world, there will be many instances where using flatMap
will make your code way more readable and less error-prone.
And an example of all this is the last snippet from the previous section, that we can now improve with the use of flatMap
:
var far1 = ["1","2","3","a"]
var far1m = far1.flatMap {
Int($0)
}
far1m /* [Int] with content [1, 2, 3] */
far1m = far1.flatMap {
Int($0)
}
.map {$0 * 2}
far1m /* [Int] with content [2, 4, 6] */
I may look just a minimal improvement in this context, but with longer chain it would become something that greatly improves readability.
And let me reiterate this again, in this context too, the behavior of swift flatMap is aligned to the bind
operation on Monads (and “flatMap” is usually used as a synonym of “bind”), you can learn more about this reading here and here.
Learn more about Sequence and IteratorProtocol protocols in the next article in the series.
Drawing inspired by emacs-utils documentation.
Did you like this article? Let me know on Twitter!