The Swift programming functions Map, Reduce and Filter can be challenging to wrap your head around. Especially, if you’ve always written for-loops to solve iteration problems!
The Map, Reduce and Filter functions come from the realm of functional programming. In Swift, you can use Map, Reduce and Filter to loop over collection types like
Array
and Dictionary
without using a for-loop.
When building apps you typically use the procedural or object-oriented way of programming. Functional programming is different: it only deals with functions. No variables, no “states”, no for-loops – just functions.
The Swift programming language lends itself perfectly for functional programming. You don’t need to strictly write functional code though, simply adopting concepts from functional programming (like Map, Reduce and Filter) can help you learn how to code better.
Map, Reduce and Filter are often called higher-order functions, because these functions take a function (a closure) as input and return functions as output. Strictly speaking, Swift will return the results of an operation (i.e. a mapped array) when using higher-order functions, whereas a “pure” functional language will return a collection of functions.
In case you’re thinking: “Dude, I don’t need functional programming or data processing, because my apps don’t do that!” – don’t stop here.
In recent app projects I’ve used Map, Reduce and Filter on multiple occasions:
- Filtering cost/revenue values, to meet a certain threshold, before including them in a line graph
- Reducing thousands of movie ratings into one average rating
- Mapping a few operations (lowercase, remove “#”) on hashtags in this Photo App Template
You could of course have solved all these problems with a for-loop, but you’ll see that using map, reduce and filter functions results in more concise, readable and performant code.
In this guide you’ll learn how to use the Map, Reduce and Filter functions. You’ll perform these functions on collection types, like
Array
.
Here’s a brief overview:
- The
map
function loops over every item in a collection, and applies an operation to each element in the collection - The
reduce
function loops over every item in a collection, and combines them into one value - The
filter
function loops over every item in a collection, and returns a collection containing only items that satisfy an include condition
Said differently:
- The
map
function applies a function to every item in a collection. Think of “mapping” one set of values into another set of values. - The
reduce
function turns a collection into one value. Think of it as literally reducing multiple values to one value. - The
filter
function simply returns an array of values that passed anif
-statement condition, and only if that condition resulted intrue
.
Fun fact: MapReduce is a fundamental Big Data processing concept, in which an intensive operation is applied in parallel to a collection. An example would be summarizing the page of a book into one word (mapping) and then storing those words in alphabetical boxes (reducing).
Skip ahead to the relevant chapters:
- Using the Map Function
- Using the Reduce Function
- Using the Filter Function
- Chaining Map, Reduce and Filter
- Conclusion
Ready? Let’s go!
The
map
function loops over every item in a collection, and applies an operation to each element in the collection. It returns an array of resulting items, to which the operation was applied.
Let’s look at an example. Say you have an array of temperatures in Celcius that you want transformed to Fahrenheit.
You could use a for-loop:
let celcius = [-5.0, 10.0, 21.0, 33.0, 50.0]
var fahrenheit:[Double] = []
for value in celcius {
fahrenheit += [value * (9/5) + 32]
}
print(fahrenheit)
// Output: [23.0, 50.0, 69.8, 91.4, 122.0]
Although the code works fine, it isn’t the most efficient. You need a mutable “helper” variable
fahrenheit
to store the calculated conversions as you work through them, and you need 3 lines of code for the conversion itself.
Check out the code sample, using the
map
function:let celcius = [-5.0, 10.0, 21.0, 33.0, 50.0]
let fahrenheit = celcius.map { $0 * (9/5) + 32 }
print(fahrenheit)
// Output: [23.0, 50.0, 69.8, 91.4, 122.0]
What happens here?
- First, an constant
celcius
is defined, of typeArray
ofDouble
, and initialized with a few random Celcius values. - Second, the function
map
is called on the constantcelcius
. The function has one argument, a closure, which converts from Celcius to Fahrenheit. - Finally, the result is printed out.
Let’s take a closer look at the closure. If you’ve worked with closures before, you’ll recognize the short-hand closure syntax. This is a shorter way to code a closure, and leaves out much of the syntax of the closure.
You basically only work with input, as
$0
, and squiggly brackets { }
. The $0
is the first input parameter for the closure, and the {
and }
signify the beginning and the end of the closure. If the closure had multiple parameters, the second parameter would be $1
, the third $2
, etcetera.
This is what the “expanded” code sample looks like:
let celcius = [-5.0, 10.0, 21.0, 33.0, 50.0]
let fahrenheit = celcius.map({ (value: Double) -> Double in
return value * (9/5) + 32
})
print(fahrenheit)
The actual
map
function call, and its closure, is this:... = celcius.map({ (value: Double) -> Double in
return value * (9/5) + 32
})
As you can see, the
map
function is called on the array celcius
. This map
function takes in one argument: the closure.
The first part of the closure, starting with
{
, indicates that this closure has one parameter value
of type Double
, and should return a value of type Double
. The closure body, starting with return
, simply returns the result of the Celcius to Fahrenheit conversion.
If you compare the short-hand closure syntax to the expanded code above, you’ll see that:
- The function parentheses
(
and)
are omitted, because you can leave those out when the last parameter of a function call is a closure. - The
() -> in
part can be omitted, because Swift can infer that you’re using oneDouble
parameter as input, and are expected to return aDouble
. Now that you’ve left out thevalue
variable, you can use the short-hand$0
. - The
return
statement can be left out too, because this closure is expected to return the result of an expression anyway.
Even though the above code sample use
Double
types, you’re not limited to these types. The resulting type of a map
function can have a different type than what you put into it, and you can use map
on a Dictionary
as well.
Here’s a more complex example, from the Photo App Template:
...
regex.matches(in: self, options: [], range: NSRange(location: 0, length: self.characters.count)).map {
string.substring(with: $0.range).replacingOccurrences(of: "#", with: "").lowercased()
}
...
In this example, the results of a regular expression are mapped, removing the
#
character, and converting the string to lowercase.
That’s the
map
function for ya!Using the Reduce Function
The
reduce
function loops over every item in a collection, and combines them into one value.Think of it as literally reducing multiple values to one value.
The Reduce function is perhaps the hardest of Map, Reduce, Filter to comprehend. How can you go from a collection of values, to one value?
A few examples:
- Creating a sum of multiple values, i.e.
3 + 4 + 5 = 12
- Concatenating a collection of strings, i.e.
["Zaphod", "Trillian", "Ford"] = "Zaphod, Trillian, Ford"
- Averaging a set of values, i.e.
(7 + 3 + 10) / 3 = 7/3 + 3/3 + 10/3 = 6.667
In data processing, you can imagine plenty scenarios when simple operations like these come in handy. Like before, you can solve any of these problems with a for-loop, but
reduce
is simply shorter and faster.
This is how you use
reduce
:let values = [3, 4, 5]
let sum = values.reduce(0, +)
print(sum)
// Output: 12
The function
reduce
takes two arguments, an initial value and a closure. As you can see in the example above, you can replace this closure by a simple expression like +
, -
or *
.
Likewise, you can also provide your own closure, with the short-hand closure syntax, like this:
let values = [7.0, 3.0, 10.0]
let avg:Double = values.reduce(0.0) { $0 + ($1 / Double(values.count)) }
print(avg)
// Output: 6.667
In the example above, you’re calculating the average of three values
7, 3, 10
. Because all the types need to match, the values
array, the avg
constant and the values.count
are of type Double
.
It’s important to note here that the closure you use for the
reduce
function has two parameters. The first one is the result of the previous reduction, i.e. the output of the function, denoted with $0
. The second one is the current item that’s being reduced, i.e. the input for the current iteration, denoted with $1
.
This input of the closure becomes clearer if you “expand” the previous example, like this:
let values = [7.0, 3.0, 10.0]
let avg = values.reduce(0.0, { (result:Double, item:Double) -> Double in
return result + (item / Double(values.count))
})
print(avg)
// Output: 6.667
In the example above you can see two closure parameters,
result
and item
, both of type Double
. This also makes clear why the reduce
function needs an initial value – that’s the result
for the first iteration of the loop!
Reduction can be challenging to grasp. You’re performing an operation on two operands, like
A + B
, but how can that have just one result? And of all the items in the collection, which items are used as operands?
Check this out:
3 + 4 + 5 + 6
((3 + 4) + 5) + 6
3 + 4 = 7
7 + 5 = 12
12 + 6 = 18
In the above example you’re using the
+
operator to add a list of numbers. On the second line you can see the order of execution, i.e. 3 + 4
is executed first. On the following lines you can see that the result of 3 + 4
is added to 5
, the third item, resulting in 12
, which is used as the left-hand operand of 12 + 6
, resulting in 18
.
In Swift code, it’s this:
let values = [3, 4, 5, 6]
let sum = values.reduce(0, +)
print(sum)
// Output: 18
In the example above, the initial value
0
is needed for the first iteration: 0 + 3 = 3
.Using the Filter Function
The
filter
function loops over every item in a collection, and returns a collection containing only items that satisfy an include condition. It’s like applying an if
-statement to a collection, and only getting the ones that pass the condition back.
Check out this example:
let values = [11, 13, 14, 17, 21, 33, 22]
let even = values.filter { $0 % 2 == 0 }
print(even)
// Output: [14, 22]
In the above example, you’re selecting numbers from
values
that are even.
The expression
$0 % 2 == 0
uses the remainder operator %
to figure out whether $0
is divisible 2
. If the remainder of the operation is 0
, the number must be even.5 % 2 == 0
// Output: false
16 % 2 == 0
// Output: true
5 / 2 leaves 1 as a remainder, and 16 / 2 leaves 0 as remainder, so therefore 16 must be an even number.
You can also see that the expressions above return a
Bool
, so true
or false
. You can assume that the closure for filter
needs to return a Bool
, and then only returns an array of values that passed the condition, i.e. returned true
.
This is the previous example, “expanded”, and only including odd numbers:
let values = [11, 13, 14, 17, 21, 33, 22]
let even = values.filter({ (value:Int) -> Bool in
return value % 2 != 0
})
print(even)
// Output: [11, 13, 17, 21, 33]
In the example, the closure should return a boolean value, indicated with
-> Bool
. It’s provided one parameter, the item in the collection, and returns the result of value % 2 != 0
.Chaining Map, Reduce and Filter
Can you combine the Map, Reduce and Filter functions? Sure you can!
Let’s say we have a class of students, and of each student you know their birth year. You want to sum the ages of all students born in or after the year 2000.
This is how you do that:
let years = [1989, 1992, 2003, 1970, 2014, 2001, 2015, 1990, 2000, 1999]
let sum = years.filter({ $0 >= 2000 }).map({ 2017 - $0 }).reduce(0, +)
print(sum)
// Output: 52
The above code sample uses chaining, in which you chain functions, and use the result of one function as input for another.
In other words,
map
function is called on the result array of the filter
function, and the reduce
function is called on the result array of the map
function.
The code itself is pretty simple:
- First, you declare an array of arbitrary birth years as integer numbers.
- Then, you filter that array with the
filter
function. If the value is greater than or equal to2000
, it is included in the result array. It’s important to note here that the size of the result is smaller than the size ofyears
, because values like1989
aren’t included! - Then, you perform an operation on each of the items with the
map
function, calculating the student’s age in the year 2017. - Finally, you reduce the collection to one value with the
+
operation, creating a sum of all ages.rt for Free
Conclusion
Now, what if you were to code this all with for-loops? You’d need at least 3 for-loops, one if-statement, and 3 sets of helper collection variables. With Map, Reduce and Filter you’ve done it in 2 lines of code, without losing out on readability and performance. Awesome!
The Map, Reduce and Filter functions have equally powerful implementations in many programming languages, including Java, JavaScript, Ruby and PHP. Learning how to process data with functional programming concepts can be challenging, but it’ll definitely save you time and make your code more concise, readable and performant.
Learn more? Here are the official Apple docs on Map, Reduce and Filter:
Enjoyed this article? Please share!
Comments
Post a Comment