Chapters

Hide chapters

Swift Apprentice

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 10 chapters
Show chapters Hide chapters

8. Collection Iteration with Closures
Written by Matt Galloway

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Earlier, you learned about functions. But Swift has another object you can use to break up code into reusable chunks: a closure. They become particularly useful 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 useful closures can be.

Closure basics

Closures are so named because they have the ability to “close over” the variables and constants within the closure’s own scope. This simply means that a closure can access, store and manipulate the value of any variable or constant from the surrounding context. Variables and constants used within the body of a closure are said to have been captured by the closure.

You may ask, “If closures are functions without names, then how do you use them?” To use a closure, you first have to 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 exactly the same as a variable declaration for a function. Like I said, a closure is simply a function without a name. The type of a closure is a function type.

In order 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 looks similar to a function declaration, but there’s a subtle difference. There’s the same parameter list, -> symbol and return type. But in the case of 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

Compared to functions, closures are designed to be lightweight. There are many ways to shorten their syntax. 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
}
multiplyClosure = { (a, b) in
  a * b
}
multiplyClosure = {
  $0 * $1
}
func operateOnNumbers(_ a: Int, _ b: Int,
                      operation: (Int, Int) -> Int) -> Int {
  let result = operation(a, b)
  print(result)
  return result
}
let addClosure = { (a: Int, b: Int) in
  a + b
}
operateOnNumbers(4, 2, operation: addClosure)
func addFunction(_ a: Int, _ b: Int) -> Int {
  a + b
}
operateOnNumbers(4, 2, operation: addFunction)
operateOnNumbers(4, 2, operation: { (a: Int, b: Int) -> Int in
  return a + b
})
operateOnNumbers(4, 2, operation: { $0 + $1 })
operateOnNumbers(4, 2, operation: +)
operateOnNumbers(4, 2) {
  $0 + $1
}

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()

Capturing from the enclosing scope

Finally, let’s return to the defining characteristic of a closure: it can access the variables and constants from within its own scope.

var counter = 0
let incrementCounter = {
  counter += 1
}
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
func countingClosure() -> () -> Int {
  var counter = 0
  let incrementCounter: () -> Int = {
    counter += 1
    return counter
  }
  return incrementCounter
}
let counter1 = countingClosure()
let counter2 = countingClosure()

counter1() // 1
counter2() // 1
counter1() // 2
counter1() // 3
counter2() // 2

Custom sorting with closures

Closures come in handy when you start looking deeper at collections. In Chapter 7, 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"]
names.sorted {
  $0.count > $1.count
}
// ["ZZZZZZ", "EEEEE", "CCCC", "BB", "A"]

Iterating over collections with closures

In Swift, collections implement some very handy features often associated with functional programming. These features come in the shape of functions that you can apply to a collection to perform an operation on it.

let values = [1, 2, 3, 4, 5, 6]
values.forEach { 
  print("\($0): \($0*$0)")
}
var prices = [1.5, 10, 4.99, 2.30, 8.19]

let largePrices = prices.filter {
  $0 > 5
}
func filter(_ isIncluded: (Element) -> Bool) -> [Element]
let largePrice = prices.first {
  $0 > 5
}
let salePrices = prices.map {
  $0 * 0.9
}
let userInput = ["0", "11", "haha", "42"]

let numbers1 = userInput.map {
  Int($0)
}
let numbers2 = userInput.compactMap {
  Int($0)
}
let sum = prices.reduce(0) {
  $0 + $1
}
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)
}
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)
  }
}
let removeFirst = prices.dropFirst()
let removeFirstTwo = prices.dropFirst(2)
removeFirst = [10, 4.99, 2.30, 8.19]
removeFirstTwo = [4.99, 2.30, 8.19]
let removeLast = prices.dropLast()
let removeLastTwo = prices.dropLast(2)
removeLast = [1.5, 10, 4.99, 2.30]
removeLastTwo = [1.5, 10, 4.99]
let firstTwo = prices.prefix(2)
let lastTwo = prices.suffix(2)
firstTwo = [1.5, 10]
lastTwo = [2.30, 8.19]
prices.removeAll() { $0 > 2 } // prices is now [1.5]
prices.removeAll() // prices is now an empty array

Lazy collections

Sometimes you can have a collection that is huge, or perhaps even infinite, but you want to be able to manipulate it in some way. A concrete example of this would be all of the prime numbers. That is obviously an infinite set of numbers. So how can you work with that set? Enter the lazy collection.

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) }
let primes = (1...).lazy
  .filter { isPrime($0) }
  .prefix(10)
primes.forEach { print($0) }

Mini-exercises

  1. Create a constant array called names that contains some names as strings. Any names will do — make sure there’s more than three. Now use reduce to create a string that is the concatenation of each name in the array.
  2. Using the same names array, first filter the array to contain only names that are longer than four characters, and then create the same concatenation of names as in the above exercise. (Hint: You can chain these operations together.)
  3. Create a constant dictionary called namesAndAges that contains some names as strings mapped to ages as integers. Now use filter to create a dictionary containing only people under the age of 18.
  4. Using the same namesAndAges dictionary, filter out the adults (those 18 or older) and then use map to 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 if you try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed 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.

func repeatTask(times: Int, task: () -> Void)

Challenge 2: Closure sums

In this challenge, you’re going to write a function that you can reuse to create different mathematical sums.

func mathSum(length: Int, series: (Int) -> Int) -> Int

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]
]

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 a lot 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 work with large, expensive or potentially infinite collections with ease.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now