Swift: Grokking Map, flatMap, and compactMap

Milan Brankovic
The Startup
4 min readJul 19, 2020

--

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 Optionals.

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 values
  • flatMap does the same thing, but also flattens the result of nested collections into just a single array
  • when an Optional is nil, map and flatMap will ignore any chained operations
  • with compactMap you can filter nil values out of arrays and sequences of Optionals
  • 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

--

--