8.
Collection Iteration With Closures
Written by Matt Galloway
Earlier, you learned about functions. But Swift has another object you can use to break up code into reusable chunks: a closure. They become instrumental when dealing with collections.
A closure is simply a function with no name; you can assign it to a variable and pass it around like any other value. This chapter shows you how convenient and valuable closures can be.
Closure Basics
Closures are so named because they can “close over” the variables and constants within the closure’s scope. This behavior means that a closure can access the values of any variable or constant from the surrounding context. Variables and constants used within the closure body are said to have been captured by the closure.
You may ask, “If closures are functions without names, how do you use them?” To use a closure, you must assign it to a variable or constant.
Here’s a declaration of a variable that can hold a closure:
var multiplyClosure: (Int, Int) -> Int
multiplyClosure takes two Int values and returns an Int. Notice that this is the same as a variable declaration for a function. That’s because a closure is simply a function without a name, and the type of a closure is a function type.
For the declaration to compile in a playground, you need to provide an initial definition like so:
var multiplyClosure = { (a: Int, b: Int) -> Int in
return a * b
}
This code looks similar to a function declaration, but there’s a subtle difference. There’s the same parameter list, -> symbol and return type. But with closures, these elements appear inside braces, and there is an in keyword after the return type.
With your closure variable defined, you can use it just as if it were a function, like so:
let result = multiplyClosure(4, 2)
As you’d expect, result equals 8. Again, though, there’s a subtle difference.
Notice how the closure has no external names for the parameters. You can’t set them like you can with functions.
Shorthand Syntax
There are many ways to shorten the syntax of a closure. First, just like normal functions, if the closure consists of a single return statement, you can leave out the return keyword like so:
multiplyClosure = { (a: Int, b: Int) -> Int in
a * b
}
Next, you can use Swift’s type inference to shorten the syntax even more by removing the type information:
multiplyClosure = { (a, b) in
a * b
}
Remember, you already declared multiplyClosure as a closure taking two Ints and returning an Int, so you can let Swift infer these types for you.
And finally, you can even omit the parameter list if you want. Swift lets you refer to each parameter by number, starting at zero, like so:
multiplyClosure = {
$0 * $1
}
The parameter list, return type and in keyword are all gone, and your new closure declaration is much shorter than the original. Numbered parameters like this should only be used when the closure is short and sweet, like the one above.
If the parameter list is longer, it can be confusing to remember what each numbered parameter refers to. In these cases, you should use the named syntax.
Consider the following code:
func operateOnNumbers(_ a: Int, _ b: Int,
operation: (Int, Int) -> Int) -> Int {
let result = operation(a, b)
print(result)
return result
}
This example declares a function named operateOnNumbers, which takes Int values as its first two parameters. The third parameter is named operation and is of a function type. operateOnNumbers itself returns an Int.
You can then use operateOnNumbers with a closure like so:
let addClosure = { (a: Int, b: Int) in
a + b
}
operateOnNumbers(4, 2, operation: addClosure)
Remember, closures are simply functions without names. So you shouldn’t be surprised to learn that you can also pass in a function as the third parameter of operateOnNumbers, like so:
func addFunction(_ a: Int, _ b: Int) -> Int {
a + b
}
operateOnNumbers(4, 2, operation: addFunction)
operateOnNumbers is called the same way, whether the operation is a function or a closure.
The power of the closure syntax comes in handy again. You can define the closure inline with the operateOnNumbers function call like this:
operateOnNumbers(4, 2, operation: { (a: Int, b: Int) -> Int in
return a + b
})
There’s no need to define the closure and assign it to a local variable or constant. You can declare the closure right where you pass it into the function as a parameter!
But recall that you can simplify the closure syntax to remove a lot of the boilerplate code. You can therefore reduce the above to the following:
operateOnNumbers(4, 2, operation: { $0 + $1 })
You can even go a step further. The + operator is just a function that takes two arguments and returns one result so that you can write:
operateOnNumbers(4, 2, operation: +)
There’s one more way to simplify the syntax, but it can only be done when the closure is the final parameter passed to a function. In this case, you can move the closure outside of the function call:
operateOnNumbers(4, 2) {
$0 + $1
}
This code may look strange, but it’s the same as the previous code snippet, except you’ve removed the operation label and pulled the braces outside the function call parameter list. This form is called trailing closure syntax.
Multiple Trailing Closures Syntax
If a function has multiple closures for inputs, you can call it in a special shorthand way. Suppose you have this function:
func sequenced(first: ()->Void, second: ()->Void) {
first()
second()
}
Swift lets you call it like so:
sequenced {
print("Hello, ", terminator: "")
} second: {
print("world.")
}
This example prints, “Hello, world.”
Note: If you ever need to remember how to call a function with a closure, Xcode can help you. Type in the method’s name (or code complete it) and press the return key twice. The code completion function will fill out trailing closure syntax for you.
Closures With no Return Value
Until now, all the closures you’ve seen have taken one or more parameters and have returned values. But just like functions, closures aren’t required to do these things. Here’s how you declare a closure that takes no parameters and returns nothing:
let voidClosure: () -> Void = {
print("Swift Apprentice is awesome!")
}
voidClosure()
The closure’s type is () -> Void. The empty parentheses denote there are no parameters. You must declare a return type so Swift knows you’re declaring a closure. This is where Void comes in handy, and it means exactly what its name suggests: the closure returns nothing.
Note:
Voidis actually just a typealias for(). This means you could have written() -> Voidas() -> (). A function’s parameter list, however, must always be surrounded by parentheses, soVoid -> ()orVoid -> Voidare invalid.
Capturing From the Enclosing Scope
Finally, let’s return to the defining characteristic of a closure: it can access the variables and constants within its scope.
Note: Recall that scope defines the range in which an entity (variable, constant, etc.) is accessible. You saw a new scope introduced with
if-statements. Closures also introduce a new scope and inherit all entities visible to the scope in which it is defined.
For example, take the following closure:
var counter = 0
let incrementCounter = {
counter += 1
}
incrementCounter is relatively simple: It increments the counter variable. The counter variable is defined outside of the closure. The closure can access the variable because the closure is defined in the same scope as the variable. The closure is said to capture the counter variable. Any changes it makes to the variable are visible both inside and outside the closure.
Let’s say you call the closure five times, like so:
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
After these five calls, counter will equal 5.
The fact that closures can be used to capture variables from the enclosing scope can be extremely useful. For example, you could write the following function:
func countingClosure() -> () -> Int {
var counter = 0
let incrementCounter: () -> Int = {
counter += 1
return counter
}
return incrementCounter
}
This function takes no parameters and returns a closure. The closure it returns takes no parameters and returns an Int.
The closure returned from this function will increment its internal counter each time it is called. Each time you call this function, you get a different counter.
For example, this could be used like so:
let counter1 = countingClosure()
let counter2 = countingClosure()
counter1() // 1
counter2() // 1
counter1() // 2
counter1() // 3
counter2() // 2
The two counters created by the function are mutually exclusive and count independently. Neat!
Custom Sorting With Closures
Closures come in handy when you start looking deeper at collections. In Chapter 7, “Arrays, Dictionaries & Sets”, you used array’s sort method to sort an array. By specifying a closure, you can customize how things are sorted. You call sorted() to get a sorted version of the array as so:
let names = ["ZZZZZZ", "BB", "A", "CCCC", "EEEEE"]
names.sorted()
// ["A", "BB", "CCCC", "EEEEE", "ZZZZZZ"]
By specifying a custom closure, you can change how the array is sorted. Specify a trailing closure like so:
names.sorted {
$0.count > $1.count
}
// ["ZZZZZZ", "EEEEE", "CCCC", "BB", "A"]
Now the array is sorted by the length of the string, with longer strings coming first.
Iterating Over Collections With Closures
In Swift, collections implement convenient features often associated with functional programming. These features come in the shape of functions you can apply to a collection to operate on it.
Operations include things like transforming each element or filtering out certain elements.
All of these functions use closures, as you will see now.
The first of these functions lets you loop over the elements in a collection and perform an operation like so:
let values = [1, 2, 3, 4, 5, 6]
values.forEach {
print("\($0): \($0*$0)")
}
This loops through each item in the collection printing the value and its square.
Another function allows you to filter out certain elements, like so:
var prices = [1.5, 10, 4.99, 2.30, 8.19]
let largePrices = prices.filter {
$0 > 5
}
Here, you create an array of Double to represent the prices of items in a shop. You use the filter function to filter out prices greater than $5. This function looks like so:
func filter(_ isIncluded: (Element) -> Bool) -> [Element]
This definition says that filter takes a single parameter, a closure (or function) that takes an Element and returns a Bool. The filter function then returns an array of Elements. In this context, Element refers to the type of items in the array. In the example above, Doubles.
The closure’s job is to return true or false depending on whether or not the value should be included. The array returned from filter will contain all elements for which the closure returned true.
In this example, largePrices will contain the following:
(10, 8.19)
Note: The array returned from
filter(and all of these functions) is a new array. The original is not modified at all.
If you’re only interested in the first element that satisfies a certain condition, you can use first(where:). For example, using a trailing closure:
let largePrice = prices.first {
$0 > 5
}
In this case, largePrice would be 10.
However, there is more!
Imagine having a sale and wanting to discount all items to 90% of their original price. There’s a handy function named map that can achieve this:
let salePrices = prices.map {
$0 * 0.9
}
The map function will take a closure, execute it on each item in the array and return a new array containing each result with the order maintained. In this case, salePrices will contain the following:
[1.35, 9, 4.491, 2.07, 7.371]
The map function can also be used to change the type. You can do that like so:
let userInput = ["0", "11", "haha", "42"]
let numbers1 = userInput.map {
Int($0)
}
This code takes some strings that the user input and turns them into an array of Int?. They must be optional because the conversion from String to Int might fail.
If you want to filter out the invalid (missing) values, you can use compactMap like so:
let numbers2 = userInput.compactMap {
Int($0)
}
This form is almost the same as map except it creates an array of Int and tosses out the missing values that fail to initialize as integers.
There’s also a flatMap operation which has a similar name to map and compactMap. However, it does something a little different. Here it is in action:
let userInputNested = [["0", "1"], ["a", "b", "c"], ["🐕"]]
let allUserInput = userInputNested.flatMap {
$0
}
You will notice that allUserInput is ["0", "1", "a", "b", "c", "🐕"].
Swift expects the return value from the closure given to flatMap to be a collection itself. What it does then takes all these collections and concatenates them together. So, in this case, it’s done the trick of unwrapping those inner collections. We end up with a collection containing all the items from the first inner collection, then all the items from the second inner collection, and so on.
Another handy function is reduce. This function takes an initial value and a closure that gets called for each element in the array. Each time the closure is called, it gets two inputs: the current value (that starts as the initial value) and an array element. The closure returns what will be the next current value. This process might sound convoluted, but an example will make it clear.
For example, this could be used with the prices array to calculate the total, like so:
let sum = prices.reduce(0) {
$0 + $1
}
The initial value representing a running total is 0. The closure gets called for each element and returns the running total plus the current element. The returned value is the new running total. The final result is the total of all the values in the array. In this case, sum will be:
26.98
Now that you’ve seen filter, map and reduce, hopefully, you realize how powerful these functions can be, thanks to the syntax of closures. You can perform a complex calculation iterating over a collection in just a few lines of code.
These functions can use any collection type, including dictionaries. Imagine you represent the stock in your shop with a dictionary mapping the price to the number of items at that price. You could use that to calculate the total value of your stock like so:
let stock = [1.5: 5, 10: 2, 4.99: 20, 2.30: 5, 8.19: 30]
let stockSum = stock.reduce(0) {
$0 + $1.key * Double($1.value)
}
The second parameter to the reduce function is a named tuple containing the key and value from the dictionary elements. A type conversion of the value is required to perform the calculation.
Here, the result is:
384.5
There’s another form of reduce named reduce(into:_:). You’d use it when the result you’re reducing a collection into is an array or dictionary, like so:
let farmAnimals = ["🐎": 5, "🐄": 10, "🐑": 50, "🐶": 1]
let allAnimals = farmAnimals.reduce(into: []) {
(result, this: (key: String, value: Int)) in
for _ in 0 ..< this.value {
result.append(this.key)
}
}
It works the same way as the other version, except that you don’t return something from the closure. Instead, each iteration gives you a mutable value. This way, only one array in this example is created and appended to, making reduce(into:_:) more efficient.
Should you need to chop up an array, a few more functions can be helpful. The first function is dropFirst, which works like so:
let removeFirst = prices.dropFirst()
let removeFirstTwo = prices.dropFirst(2)
The dropFirst function takes a single parameter that defaults to 1 and returns an array with the required number of elements removed from the front. The results are as follows:
removeFirst = [10, 4.99, 2.30, 8.19]
removeFirstTwo = [4.99, 2.30, 8.19]
Like dropFirst, there also exists dropLast, which removes elements from the end of the array. It works like this:
let removeLast = prices.dropLast()
let removeLastTwo = prices.dropLast(2)
The results of these are as you would expect:
removeLast = [1.5, 10, 4.99, 2.30]
removeLastTwo = [1.5, 10, 4.99]
You can select just the first or last elements of an array, as shown below:
let firstTwo = prices.prefix(2)
let lastTwo = prices.suffix(2)
Here, prefix returns the required number of elements from the front of the array, and suffix returns the required number of elements from the back of the array. The results of this function are:
firstTwo = [1.5, 10]
lastTwo = [2.30, 8.19]
And finally, you can remove all elements in a collection by using removeAll() qualified by a closure, or unconditionally:
prices.removeAll() { $0 > 2 } // prices is now [1.5]
prices.removeAll() // prices is now an empty array
Lazy Collections
Sometimes you can have a huge or even infinite collection, but you want to be able to access it somehow. A concrete example of this would be all of the prime numbers. That is an infinite set of numbers. So how can you work with that set? Enter the lazy collection. Consider that you might want to calculate the first ten prime numbers. To do this imperatively, you might do something like this:
func isPrime(_ number: Int) -> Bool {
if number == 1 { return false }
if number == 2 || number == 3 { return true }
for i in 2...Int(Double(number).squareRoot()) {
if number % i == 0 { return false }
}
return true
}
var primes: [Int] = []
var i = 1
while primes.count < 10 {
if isPrime(i) {
primes.append(i)
}
i += 1
}
primes.forEach { print($0) }
This example defines a function that checks whether a number is prime. Then it generates an array of the first ten prime numbers.
Note: The function to calculate if this is a prime could be better! Calculating primes is a deep topic beyond this chapter’s scope. If you’re curious, I suggest reading about the Sieve of Eratosthenes.
This code works, but functional is better, as you saw earlier in the chapter. The functional way to get the first ten prime numbers would be to have a sequence of all the prime numbers and then use prefix() to get the first ten. However, how can you have a sequence of infinite length and get the prefix() of that? That’s where you can use the lazy operation to tell Swift to create the collection on-demand when it’s needed.
Let’s see it in action. You could rewrite the code above instead like this:
let primes = (1...).lazy
.filter { isPrime($0) }
.prefix(10)
primes.forEach { print($0) }
Notice that you start with the open-ended collection 1..., which means 1 until, well, infinity (or rather the maximum integer that the Int type can hold!). Then you use lazy to tell Swift that you want this to be a lazy collection. Then you use filter() and prefix() to filter out the primes and choose the first ten.
At that point, the sequence has yet to be generated, and no numbers have been checked to be prime. Only with the second statement, the primes.forEach is the sequence evaluated, and the first ten prime numbers are evaluated and printed. Neat! :]
Lazy collections are instrumental when the collection is huge (even infinite) or expensive to generate. It saves the computation until precisely when it is needed.
That wraps up collection iteration with closures!
Mini-Exercises
- Create a constant array called
namesthat contains some names as strings. Any names will do — make sure there are more than three. Now usereduceto create a string that is the concatenation of each name in the array. - Using the same
namesarray, first filter the array to contain only names longer than four characters and then create the same concatenation of names as in the above exercise. (Hint: You can chain these operations together.) - Create a constant dictionary called
namesAndAgescontaining some names as strings mapped to ages as integers. Now usefilterto create a dictionary containing only people under the age of 18. - Using the same
namesAndAgesdictionary, filter out the adults (those 18 or older) and then usemapto convert to an array containing just the names (i.e., drop the ages).
Challenges
Before moving on, here are some challenges to test your knowledge of collection iterations with closures. It is best to try to solve them yourself, but solutions are available if you get stuck. Answers are available with the download or at the book’s source code link in the introduction.
Challenge 1: Repeating Yourself
Your first challenge is to write a function that will run a given closure a given number of times.
Declare the function like so:
func repeatTask(times: Int, task: () -> Void)
The function should run the task closure, times number of times. Use this function to print "Swift Apprentice is a great book!" 10 times.
Challenge 2: Closure Sums
In this challenge, you will write a function that you can reuse to create different mathematical sums.
Declare the function like so:
func mathSum(length: Int, series: (Int) -> Int) -> Int
The first parameter, length, defines the number of values to sum. The second parameter, series, is a closure that can be used to generate a series of values. series should have a parameter that is the position of the value in the series and return the value at that position.
mathSum should calculate length number of values, starting at position 1, and return their sum.
Use the function to find the sum of the first 10 square numbers, which equals 385. Then use the function to find the sum of the first 10 Fibonacci numbers, which equals 143. For the Fibonacci numbers, you can use the function you wrote in Chapter 5, “Functions” — or grab it from the solutions if you’re unsure your solution is correct.
Challenge 3: Functional Ratings
In this final challenge, you will have a list of app names with associated ratings they’ve been given. Note — these are all fictional apps! Create the data dictionary like so:
let appRatings = [
"Calendar Pro": [1, 5, 5, 4, 2, 1, 5, 4],
"The Messenger": [5, 4, 2, 5, 4, 1, 1, 2],
"Socialise": [2, 1, 2, 2, 1, 2, 4, 2]
]
First, create a dictionary called averageRatings that will contain a mapping of app names to average ratings. Use forEach to iterate through the appRatings dictionary, then use reduce to calculate the average rating. Store this rating in the averageRatings dictionary. Finally, use filter and map chained together to get a list of the app names whose average rating is greater than 3.
Key Points
- Closures are functions without names. They can be assigned to variables and passed as parameters to functions.
- Closures have shorthand syntax that makes them easier to use than other functions.
- A closure can capture the variables and constants from its surrounding context.
- A closure can be used to direct how a collection is sorted.
- A handy set of functions exists on collections that you can use to iterate over a collection and transform it. Transforms comprise mapping each element to a new value, filtering out certain values and reducing the collection down to a single value.
- Lazy collections can be used to evaluate a collection only when strictly needed, which means you can efficiently work with large, expensive or potentially infinite collections.