Waldo joins Tricentis, expanding mobile testing for higher-quality mobile apps – Learn more
App Development

Understanding map, filter, reduce and other higher order functions in Swift

Donny Wals
Donny Wals
Understanding map, filter, reduce and other higher order functions in Swift
December 15, 2022
8
min read

When we’re writing code it’s not uncommon to encounter situations where we need to take some value and transform that value into another value. Sometimes this conversion can be very obvious, for example when we want to take an array of [String] and transform it into an array of [URL]. At other times, the transformation can be less obvious. For example, when you want to get the average length of the strings in an array of [String].

Swift comes with various useful functions that allow us to perform such operations. The cool thing is that these functions are in no way unique to Swift. They are deeply rooted in functional programming and are implemented as so-called higher order functions.

In this post, you will learn what higher order functions are, and how they work. We’ll start by looking at some examples of common higher order functions in action.

Using higher order functions in Swift

There are many higher order functions available in Swift, and showing them all wouldn’t make a lot of sense because they all operate in similar ways. So instead, let’s have a look at a commonly used higher order function; map.

A map is always used to take a value of one type, and transform it into another value. For example, we could transform an array of [String] to [URL] as follows:


let strings = ["waldo.com", "donnywals.com"]
let urls = strings.map { string in 
  return URL(string: "https://\(string)")!
}

The map function that we use here is available on any Collection type in Swift. The function takes a closure that is called for every element in the Collection, and you’re expected to return a new element from your closure. The returned element will take the place of the original element in the Collection.

In other words, map allows you to transform each value in a Collection to a new, different, value.

The concept of mapping is not unique to collections. For example, you can also use map on Swift’s Optional:


let exampleString: Optional<String> = "Hello, world"
let count = exampleString.map { string in
    return string.count
}

The map that’s defined on Optional is only evaluated if the Optional has a non-nil value. Using a map on Optional is far less common but in can, in certain cases, be a lot easier to read than alternatives where you either have to use a ternary expression or unwrap the property and conditionally assign a value to a variable:


let exampleString: Optional<String> = "Hello, world"
let count: Int?

if let string = exampleString {
	count = string.count
}

The result of the code above would be the same, but it’s a lot more obscure in terms of what the code is intended to do than mapping over exampleString.

Another common example of a higher order function that’s commonly used in Swift is filter. For example, we could extract all strings from an array that are longer than a certain number of characters as follows:


let strings = ["Hello", "World", "These", "Are",
               "Some", "Strings", "In", "An",
               "Array"]

let long = strings.filter { string in
    return string.count >= 5
}

Similar to how map works, the filter function iterates over our array of strings and executes a closure for each element in the array. Depending on whether we return true or false, the original element is included in the filtered array.

While filter does something completere different from map, the pattern is the same. We call a function on a Collection, we pass a closure to that function, that closure is evaluated for each element in the Collection and the end result is a whole new Collection.

Let’s take a look at a slightly different kind of higher order function before we actually get to a formal definition of higher order functions.

There are times where you want to distil a collection of elements down to a single value. For example, you might want to figure out the average grade from a list of test results. You could approach that problem as follows:


struct TestResult {
    let grade: Double
    let student: Student
}

let results: [TestResult] = [/* ... */]

let grades = results.map { result in
    return result.grade
}

var totalGrade: Double = 0

grades.forEach { grade in
    totalGrade += grade
}

let average = totalGrade / Double(grades.count)

The code above would work, and it uses a higher order function you haven’t seen before; forEach. A forEach is a lot like a for x in y loop, except we can’t bail out early and it’s defined as a higher order function on Collection. Just like map and filter are.

While the code above works, there is a much cleaner way to achieve the same result using a function called reduce:


let average: Double = results.reduce(0) { total, result in
    return total + result.grade
} / Double(results.count)

I’ve omitted the definition of TestResult and the array of results in the snippet above, but it should be pretty clear how much shorter and more concise the reduce based version of this function is.

While brevity is good, we did add (a lot of) complexity to the code by using a reduce. I’m certain that if you’re not already familiar with reduce, the code above will look pretty arcane to you so let’s pick apart what we’re doing here.

A reduce function is used to reduce a collection of many values down to a single value. So in our case, we’re using reduce to transform our [TestResult] into a Double. We do this by calling the reduce function and providing it an initial value. In our case that’s 0. The second argument that’s passed to reduce is a closure that will be called for every element in our collection.

Inside of our closure, we’ll receive two arguments:

  1. The current “total” value that we’ve accumulated
  2. The element in the collection that we’re processing

The logic in the closure is expected to extract and compute what we need based on the accumulated value (the total) and the current element. We then return the result of our computation, and that result will become the new accumulated value.

In our case, the processing is pretty simple. We return the accumulated value incremented by the grade for each test result. The outcome is a number that’s the total of all grades in our array of test results.

To get the average, we divide the total by the number of test results we’ve processed.

In the code you saw earlier, I put this all down as a single statement which might not be the easiest thing to read.

If we split this out into two separate statements, we’d write the code as follows:


let total: Double = results.reduce(0) { total, result in
    return total + result.grade
} 
let average = total / Double(results.count)

The result is the same, but it’s a little bit easier to see what’s happening when we split the code out over multiple lines.

If you’re wondering what other higher functions are available to us, I highly recommend to take a look at the documentation for Collection. You’ll find various instance methods that accept closures and return a new collection.

Now that you’ve seen higher order functions in action, and now that you understand why certain higher order functions are useful, let’s take a broader look at higher order functions and find out what they are exactly.

Understanding what makes a function higher order

The examples you’ve seen so far all have same things in common:

  • They are called on some object, often a Collection but we also saw one that’s defined on Optional
  • They take a closure that is evaluated with an element

It’s pretty clear that there’s a pattern with all of the functions we’ve seen but not everything that we’ve seen is relevant to higher order functions.

In reality, the one thing that makes the functions you’ve seen higher order is the fact that they take a closure as one of their arguments. Any function that takes a closure as an argument is theoretically considered a higher order function.

This means that functions that take a completion handler, like URLSession's dataTask function are also considered higher order functions. We generally don’t think of these functions whenever higher order functions come up as a topic because they’re not part of the classic examples from functional programming like map or flatMap.

Another thing that can make a function higher order is if a function returns another function (or closure) as its return type.

In Swift, functions that returns functions aren’t commonly used but there is no reason why you wouldn’t be able to write the following:


func tableOf(_ root: Int) -> (Int) -> Int {
    return { multiplier in
        return root * multiplier
    }
}

let tableOfEight = tableOf(8)
tableOfEight(1) // 8
tableOfEight(2) // 16
tableOfEight(3) // 24

Patterns like the above or very common in functional programming languages. However, you don’t see them very often in Swift even though it’s a very useful and powerful tool in your toolbox to write code like this.

So in short, a function is higher order if:

  • It takes a closure (or function) as one of its arguments
  • It returns a closure (or function)

In Summary

In this post, you learned everything you need to know about common higher order functions in Swift. We started off by looking at some common higher order functions in Swift, and we explored how they are used. After that, you learned about what makes a function higher order, and you saw one more kind of higher order function that you haven’t learned before.

Now that you know what a higher order function is, there’s one last thing for you to know. Understanding the patterns behind higher order functions (like passing around closures to functions such as map) is far more important than knowing that map is a higher order function.

Knowing the name of this pattern is useful in job interviews for example, because that will show that you know some theory behind a common pattern in Swift. And of course, sometimes knowing that certain things have a name makes it much easier to learn more and dig deeper into certain topics that you might be interested in.

Automated E2E tests for your mobile app

Creating tests in Waldo is as easy as using your app!
Learn more about our Automate product, or try our live testing tool Sessions today.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.