In the previous post I made an introduction to higher order functions in Swift, explaining what such a function is and how to create your own. In this post I am going to focus on some specific higher order functions bundled in Swift, and I will continue doing so on the next post as well.
Today I will focus on three higher order functions. One could tell that they belong to the same family; however they have certain differences and each one comes to serve a specific purpose and use case. These are the map
, compactMap
, and flatMap
higher order functions.
It’s really important and necessary to underline that all map functions work with collections; arrays, dictionaries, and sets. It’s common, however, to see them being used more often with arrays, as that’s the kind of the collection mostly used when coding.
The map higher order function
The purpose of the map
higher order function is to go through all elements of a collection, operate on every single element, and return a new collection with the results. In other words, it gets a collection as input, it does something with it, and returns another collection.
In addition, map
function, as any other higher order function, helps us write shorter and cleaner code. That happens because with a single line of code we can replace a bunch of lines with custom implementation that would be performing the exact same operation on a collection.
The following examples will help clear all that out. To see everything in action, just open a playground in Xcode and use the code that you will be meeting along the way.
Let’s begin with the following array; it contains the names of ten random countries:
1 2 3 |
let countries = [“France”, “Germany”, “United States of America”, “United Kingdom”, “Australia”, “Greece”, “Italy”, “Spain”, “Canada”, “Egypt”] |
For the sake of the example, let’s suppose that we want to come up with a new array, where each item will be showing the length of each country name. Let’s do so with a custom implementation first, without using any higher order function:
1 2 3 4 5 6 7 |
var namesLength = [Int]() for i in 0..<countries.count { let totalChars = countries[i].count namesLength(totalChars) } |
The first step is to initialize a new array (called namesLength
above). Then, inside a for-in
loop we get the length of each name string, and then we append it to the new array.
Let’s use now the map
higher order function. To do so, we access it with the dot syntax in the original collection. It accepts a method or a closure as argument, where inside its body the custom logic is being implemented. Here we’ll be returning the length of each country name. The most common approach is to use a closure, and a closure’s body is automatically appended by following Xcode’s auto-suggestion:
1 2 3 4 5 |
namesLength = countries.map { (country) -> Int in return country.count } |
The parameter value in the closure represents each country name in the countries
array. There is also a return value; in this example we want to return an integer number that shows the length of each country name.
The above implementation is definitely shorter than the custom one, however it can become even shorter by omitting the return type and the return
keyword inside the closure’s body:
1 2 3 4 5 |
namesLength = countries.map { country in country.count } |
Even though the return type is not explicitly specified anymore, it’s automatically inferred based on the return value resulting from any calculations or operations in the closure’s body.
The above can become even shorter and simpler, and this time we will omit the parameter value. Instead, we will use a shorthand argument:
1 2 3 4 5 |
namesLength = countries.map { $0.count } |
And of course, we can write the above in one line only:
1 2 3 |
namesLength = countries.map { $0.count } |
Not only we replaced the original five lines of code with a single line, but the above statement is so much cleaner comparing to the initial implementation. map
function creates a new array, appending to each position the length of the respective country name, while iterating through all names in the countries
array behind the scenes.
All the above code snippets are equivalent and they return the following array:
1 2 3 |
[6, 7, 24, 14, 9, 6, 5, 5, 6, 9] |
Here is one more example using the map
function. It converts all country names to uppercase, appending them to a new array:
1 2 3 4 |
let uppercasedCountries = countries.map { $0.uppercased() } print(uppercasedCountries) |
The printed array is the following:
1 2 3 |
[“FRANCE”, “GERMANY”, “UNITED STATES OF AMERICA”, “UNITED KINGDOM”, “AUSTRALIA”, “GREECE”, “ITALY”, “SPAIN”, “CANADA”, “EGYPT”] |
map
higher order function is also quite useful when working with dictionaries. One of its greatest advantages is that it makes it really easy to separate keys and values, and therefore get two different arrays containing the one or the other.
Let’s take as example the following dictionary, which contains the countries from the previous examples and the continent they belong to:
1 2 3 4 5 6 7 8 9 |
var countriesAndContinents = [“France”: “Europe”, “Germany”: “Europe”, “United States of America”: “America”, “United Kingdom”: “Europe”, “Australia”: “Australia”, “Greece”: “Europe”, “Italy”: “Europe”, “Spain”: “Europe”, “Canada”: “America”, “Egypt”: “Africa”] |
The following two lines return both keys and values in two different arrays:
1 2 3 4 |
let keys = countriesAndContinents.map { $0.key } let values = countriesAndContinents.map { $0.value } |
But map
‘s usefulness does not stop in the above; we can easily get a new dictionary after having modified either keys or values and with an additional step with map
.
To showcase that, suppose that we want to come up with a new dictionary that contains all values (continents in this example) uppercased. Obviously the following would not be correct, because it returns an array with all values only:
1 2 3 |
let results = countriesAndContinents.map { $0.value.uppercased() } |
The trick that will actually work is the following:
1 2 3 |
let results = countriesAndContinents.map { ($0.key, $0.value.uppercased()) } |
See that a tuple is returned from the map
‘s closure. The first value is each key, and the second is each value uppercased. results
array is an array of tuples, and that has a purpose. We can use it by providing it as argument to a specific initializer of the Dictionary type, and therefore end up with a new dictionary:
1 2 3 |
let updatedCountriesAndContinents = Dictionary(uniqueKeysWithValues: results) |
Printing the updatedCountriesAndContinents
will show this:
1 2 3 |
[“United States of America”: “AMERICA”, “Spain”: “EUROPE”, “Canada”: “AMERICA”, “Egypt”: “AFRICA”, “Italy”: “EUROPE”, “United Kingdom”: “EUROPE”, “Germany”: “EUROPE”, “Australia”: “AUSTRALIA”, “Greece”: “EUROPE”, “France”: “EUROPE”] |
The compactMap higher order function
If you know how map
works, then in a similar way you can use both compactMap
and the flatMap
function that you’ll read about next.
compactMap
works exactly as the map
does, with one big difference though; it ignores any nil values in case they exist in the original collection.
Let’s take the countries
array as example once again, only this time nil values have replaced some actual country names:
1 2 3 |
let countriesWithNil = [“France”, nil, “United States of America”, nil, “Australia”, “Greece”, nil, “Spain”, nil, “Egypt”] |
To keep things simple, let’s assume that once again we want to get country names uppercased. Using map
, we would need to optionally unwrap the map
‘s argument:
1 2 3 |
let uppercased = countriesWithNil.map { $0?.uppercased() } |
The resulting array will be containing the following optional and nil values together:
1 2 3 |
[Optional(“FRANCE”), nil, Optional(“UNITED STATES OF AMERICA”), nil, Optional(“AUSTRALIA”), Optional(“GREECE”), nil, Optional(“SPAIN”), nil, Optional(“EGYPT”)] |
This may suit you sometimes, but if you want to get results for those countries that have an actual name in the original array, then compactMap
is what you really need to use:
1 2 3 |
let uppercased = countriesWithNil.compactMap { $0?.uppercased() } |
The resulting array contains no optionals this time, and nil values have been omitted:
1 2 3 |
[“FRANCE”, “UNITED STATES OF AMERICA”, “AUSTRALIA”, “GREECE”, “SPAIN”, “EGYPT”] |
Knowing what compactMap
does now, use that instead of map
when you want to get rid of nil values, or just neglect them in the original collection. Besides that, everything else discussed about the map
function applies here too.
The flatMap higher order function
This higher order function is handy when you need to get a single collection out of other collections contained in each other. To clarify that, imagine that the following array keeps the average temperature of a city in the morning and in the afternoon (in Celsius degrees):
1 2 3 |
let temps = [15, 18] |
Now suppose that we have seven arrays like the above into another array, matching to the seven days of the week:
1 2 3 |
let weekTemps = [[15, 18], [17.5, 16], [13.4, 14.1], [14.4, 15.2], [15, 15.8], [16.8, 17.9], [16, 18.2]] |
Now, what if we wanted to calculate the average temperature of the whole week? It would be really convenient if we could have all temperatures existed in a single array.
This is what flatMap
does; it merges values from nested collections into one, single collection. All it takes to achieve that is the following:
1 2 3 |
let allTemps = weekTemps.flatMap { $0 } |
All temperatures exist now in a single array:
1 2 3 |
[15.0, 18.0, 17.5, 16.0, 13.4, 14.1, 14.4, 15.2, 15.0, 15.8, 16.8, 17.9, 16.0, 18.2] |
The above transformation makes it much easier to process these values and do something with them.
Summary
After the initial introduction to higher order functions in the previous post, here I briefly described how to use some common and similar higher order functions provided in Swift, explaining what they are for, how they work, and how they can help to write better code. More of them are coming to the next post, so don’t miss it. Thank you for your reading time!
The next post about forEach
, filter
and sorted
higher order functions is available here.