Swift: Grokking Map, flatMap, and compactMap
Swift borrows many functional programming concepts, among others: map
, flatMap
and compactMap
. This article will give a closer look at these operations.
map
Suppose that we have detectives with: name, rank and cases working on. To find out which detectives are heavily involved in solving cases we can implement the following approach
struct Detective {
let name: String
let rank: String
let cases: UInt
}func involvedInCases(detectives: [Detective]) -> [String] {
var involvments = [String]()
for detective in detectives {
let involvment: String;
switch detective.cases {
case 0: involvment = "\(detective.name) is not involved"
case 1...10: involvment = "\(detective.name) is pretty involved"
default: involvment = "\(detective.name) is heavily involved"
}
involvments.append(involvment)
}
return involvments
}
This is a good start, but it can be improved with map
operation. With map
you can transform each value of the array into something that is desirable. map
receives a closure which gets called for each element of the array. After it finishes it returns a new mapped array.
func involvedInCases(detectives: [Detective]) -> [String] {
return detectives
.map { detective -> String in
switch detective.cases {
case 0: return "\(detective.name) is not involved"
case 1...10: return "\(detective.name) is pretty involved"
default: return "\(detective.name) is heavily involved"
}
}
}
This looks pretty good, and can be extended even more. The result of map
can be put into pipeline to perform additional transformations. Imagine that we need to get the names of detectives who are not involved in any case in order to assign them some. We can accomplish this by creating a following construct:
func getUninvovedDetectivesSorted(detectives: [Detective]) -> [String] {
return detectives
.filter { $0.cases == 0 }
.map { $0.name }
.sorted(by: <)
}
A pipeline defines a clear and immutable way of transforming data into separate steps. Generally, a pipeline is performant enough and worth the expressive and immutable nature.
An Array
is not the only collection type which you can use map
over. You can use any Collection
and Sequence
protocol implementation as well as Optional
s.
flatMap
The flatMap
function is similar to map
except that it “flattens” the resulting array. With map
you can end up with nested type, flatMap
is there to handle this case. The example below shows detective's scores on each test, and we want to check if the average result is higher than some threshold value:
let detectiveScoresByName = [
"John": [100, 35, 86],
"Nick": [22, 56, 86],
"Ron": [92, 1, 34]]
let mapped = detectiveScoresByName.map { $0.value }
// [[100, 35, 86], [22, 56, 86], [92, 1, 34]] - An array of arrays
let flatMapped = detectiveScoresByName.flatMap { $0.value }
// [100, 35, 86, 22, 56, 86, 92, 1, 34] - flattened to only one array// implementation of the check omitted
So map
transforms an array of values into an array of other values, and flatMap
does the same thing, but also flattens the result of nested collections into just a single array.
compactMap
Finally, there’s compactMap
, which lets us discard any nil
values that our transform might produce. Instead of flattening nested collection, this function performs flattening optionals inside the collection.
Let's go over an example that will shine a light on this type of operation. A user is providing a list of his/hers favorite websites, which we are trying to reach from code. We will try to turn passed strings into URLs and then perform operation:
let urls = [
"http://www.google.com",
"http://www.duckduckgo.com",
"http://www.bing.com",
"http://my search engine.com"]
let mapped = urls.map { URL(string: $0) }
// [Optional(http://www.google.com), Optional(http://www.duckduckgo.com), Optional(http://www.bing.com), nil]
let compactMapped = urls.compactMap(URL.init)
// [http://www.google.com, http://www.duckduckgo.com, http://www.bing.com]// implementation of the ping omitted
As construction of URL
can fail and return nil
, map
would not produce correct output (it will contain nil
). Additionally, map
would produce elements wrapped in Optional
which should be handled. compactMap
takes care of both problems for us.
As for map
, both flatMap
and compactMap
can be chained (used in pipeline).
Key takeaways
map
transforms an array of values into an array of other valuesflatMap
does the same thing, but also flattens the result of nested collections into just a single array- when an
Optional
isnil
,map
andflatMap
will ignore any chained operations - with
compactMap
you can filternil
values out of arrays and sequences ofOptional
s - everything can be done in imperative way as well
- if you are considering performance as a key factor, you should test both imperative and functional way in order to tailor the code to your needs