When to use map, flatMap and compactMap in Swift

Published on: October 23, 2019

Any time you deal with a list of data, and you want to transform the elements in this list to a different type of element, there are several ways for you to achieve your goal. In this week’s Quick Tip, I will show you three popular transformation methods with similar names but vastly different applications and results.

By the end of this post, you will understand exactly what map, flatMap and compactMap are and what they do. You will also be able to decide which flavor of map to use depending on your goals. Let’s dive right in by exploring the most straightforward of the three; map.

Understanding map

Any time you have a set of data where you want to transform every element of the sequence into a different element, regardless of nested lists or nil values, you’re thinking of using map. The working of map is probably best explained using an example:

let numbers = [1, 2, 3, 4, 5, 6]

let mapped = numbers.map { number in
  return "Number \(number)"
}

// ["Number 1", "Number 2", "Number 3", "Number 4", "Number 5", "Number 6"]
print(mapped)

The example shows an array of numbers from one through six. By calling map on this array, a loop is started where you can transform the number and return another number or, like in this example, something completely different. By using map, the original array of numbers is unaltered and a new array is created that contains all of the transformed elements. When you call map on an array with nested arrays, the input for the map closure will an array. Let’s look at another example:

let original = [["Hello", "world"], ["This", "is", "a", "nested", "array"]]
let joined = original.map { item in
  return item.joined(separator: " ")
}

// ["Hello world", "This is a nested array"]
print(joined)

By joining all strings in the nested array together, we get a new array with two strings instead of two nested arrays because each array was mapped to a string. But what if we wanted to do something else with these strings like for instance remove the nested arrays and get the following output:

["Hello", "world", "This", "is", "a", "nested", "array"]

This is what flatMap is good at.

Understanding flatMap

The slightly more complicated sibling of map is called flatMap. You use this flavor of map when you have a sequence of sequences, for instance, an array of arrays, that you want to "flatten". An example of this is removing nested arrays so you end up with one big array. Let’s see how this is done exactly:

let original = [["Hello", "world"], ["This", "is", "a", "nested", "array"]]
let flatMapped = original.flatMap { item in
  return item
}

// ["Hello", "world", "This", "is", "a", "nested", "array"]
print(flatMapped)

Even though the type of item is [String], and we return it without changing it, we still get a flat array of strings without any nested arrays. flatMap works by first looping over your elements like a regular map does. After doing this, it flattens the result by removing one layer of nested sequences. Let’s have a look at an interesting misuse of flatMap:

let original = [["Hello", "world"], ["This", "is", "a", "nested", "array"]]
let flatMapped2 = original.flatMap { item in
  item.joined(separator: " ")
}

print(flatMapped2)

This example joins the item (which is of type [String]) together into a single string, just like you did in the map example. I want you to think about the output of this flatMap operation for a moment. What do you expect to be printed?

I’ll give you a moment here.

If you thought that the result would be the same as you saw before with map, you expected the following result:

["Hello world", "This is a nested array"]

This makes sense, you’re returning a String from the flatMap closure. However, the actual result is the following:

["H", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "T", "h", "i", "s", " ", "i", "s", " ", "a", " ", "n", "e", "s", "t", "e", "d", " ", "a", "r", "r", "a", "y"]

Wait, what? Every letter is now an individual element in the array? That’s correct. Since String is a sequence of Character, flatMap will flatten the String, pulling it apart into its individual Character objects. If this is not the result you had in mind, that’s okay. flatMap can be a confusing beast. It’s important to keep in mind that it will flatten anything that is a sequence of sequences into a single sequence. Even if it’s a String.

Let’s see how the last flavor of map, compactMap is different from map and flatMap.

Understanding compactMap

The last flavor of map we’ll look at in this Quick Tip is compactMap. Whenever you have an array of optional elements, for instance [String?], there are cases when you want to filter out all the nil items so you end up with an array of [String] instead. It might be tempting to do this using a filter:

let arrayWithOptionals = ["Hello", nil, "World"]
let arrayWithoutOptionals = arrayWithOptionals.filter { item in
  return item != nil
}

// [Optional("Hello"), Optional("World")]
print(arrayWithoutOptionals)

This kind of works but the type of arrayWithoutOptionals is still [String?]. How about using a for loop:

let arrayWithOptionals = ["Hello", nil, "World"]
var arrayWithoutOptionals = [String]()
for item in arrayWithOptionals {
  if let string = item {
    arrayWithoutOptionals.append(string)
  }
}

// ["Hello", "World"]
print(arrayWithoutOptionals)

Effective, but not very pretty. Luckily, we have compactMap which is used for exactly this one purpose. Turning a sequence that contains optionals into a sequence that does not contain optionals:

let arrayWithOptionals = ["Hello", nil, "World"]
var arrayWithoutOptionals = arrayWithOptionals.compactMap { item in
  return item
}

// ["Hello", "World"]
print(arrayWithoutOptionals)

Neat, right? A somewhat more interesting example might be to take an array of strings and converting them into an array of URL objects. Immediately filtering out the nil results that occur when you convert an invalid String to URL:

let urlStrings = ["", "https://blog.donnywals.com", "https://avanderlee.com"]
let urls = urlStrings.compactMap { string in
  return URL(string: string)
}

// [https://blog.donnywals.com, https://avanderlee.com]
print(urls)

We now have converted the input strings to valid URL instances, omitting any nil values. Pretty neat, right.

In summary

This Quick Tip showed you three ways to convert an array or sequence of elements into a different kind of sequence. map is the most basic variant, it converts every element in the sequence into a different sequence. No exceptions. Next, we looked at flatMap which looks similar to map but it flattens the first level of nested sequences. You also saw how it would flatten an array of two strings into an array of many single characters because a String is a sequence of Character. Lastly, you learned about compactMap. This mapping function transforms all elements in a sequence into a different element, just like map, but it removes nil values afterward.

Now that you have learned about map on sequence types like arrays, you should have a vague idea of what might happen if you map a Publisher in Combine, or maybe if you call map on an Optional. If you’re not sure, I challenge to go ahead and try it out. You might find some interesting results!

Thanks for reading this post, and as always your feedback is more than welcome! Don’t hesitate to shoot me a Tweet if you have any questions, feedback or just want to reach out.

Subscribe to my newsletter