When to use map, flatMap and compactMap in Swift
Published on: October 23, 2019Any 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.