Map, Reduce and Filter in Swift

Written by Reinder de Vries on July 9 2020 in App Development, Swift

Map, Reduce and Filter in Swift

In Swift you use map(), reduce() and filter() to loop over collections like arrays and dictionaries, without using a for-loop.

The map, reduce and filter functions come from the realm of functional programming (FP). They’re called higher-order functions, because they take functions as input. You’re applying a function to an array, for example, to transform its data.

Swift’s Map, Reduce and Filter functions can challenging to wrap your head around. Especially if you’ve always coded for in loops to solve iteration problems. In this guide you’ll learn how to use the map(_:), reduce(_:_:) and filter(_:) functions in Swift.

Let’s get started!

  1. Introduction to Map, Reduce and Filter
  2. Quick Start: Higher-Order Functions in Swift
  3. Using the Map Function
  4. Using the Reduce Function
  5. Using the Filter Function
  6. Combining Map, Reduce and Filter
  7. Further Reading

Introduction to Map, Reduce and Filter

When you’re building iOS apps, you typically use procedural or Object-Oriented programming. Functional programming is different: it only deals with functions. No variables, no state, no for-loops — just functions.

The Swift programming language lends itself perfectly for mixing functional programming with non-functional approaches, like OOP. You don’t need to strictly write functional code, and adopting concepts from functional programming can help you learn how to code better.

The map(_:), reduce(_:_:) and filter(_:) functions are called higher-order functions, because they take a function as input and return functions as output. Technically, Swift returns the results of an operation (i.e. a transformed array) when using higher-order functions, whereas a pure functional language will return a collection of functions. In Swift, the inputs for these functions are closures.

Here’s how they work:

  • The map() function applies a function to every item in a collection. Think of “mapping” or transforming one set of values into another set of values.
  • The reduce() function turns a collection into one value. Think of it as combining many values into one, like averaging a set of numbers.
  • The filter() function simply returns values that passed an if-statement, and only if that condition resulted in true.

In case you’re thinking: “Look, I don’t need functional programming or data processing, because my apps don’t do that!” then don’t stop here. In recent app projects I’ve used Map, Reduce and Filter on multiple occasions:

  • Filtering cost/revenue values with filter(_:), to meet a threshold, prior to showing the values in a line graph
  • Averaging thousands of movie ratings into one value with reduce(_:_:)
  • Mapping a few operations on a string with hashtags, transforming it into a normalized collection, with map(_:)

You could have solved all these problems with a for-loop, but you’ll see that using map(), reduce() and filter() results in more concise, readable and performant code.

Quick Start: Higher-Order Functions in Swift

We’ll focus on map(), reduce() and filter() in this tutorial. Before we move on, here’s a quick overview of the most common higher-order functions in Swift:

  • map(_:) loops over every item in a sequence, applies a function to each element, and returns the transformed result
  • reduce(_:_:) loops over every item in a sequence, combines them into one value, and returns the combined result
  • filter(_:) loops over every item in a sequence and returns a resulting sequence that only contains items that satisfy a given filtering function
  • flatMap(_:) does the same as map(_:), except that it flattens the resulting sequence, i.e. nested arrays are un-nested or “flattened out”
  • compactMap(_:) does the same as map(_:), except that it removes nil values from the resulting sequence prior to returning it

You can use these functions on arrays, dictionaries, sets, ranges, sequences, and any other Swift type you can iterate over. If you want to learn more about compactMap(_:) and flatMap(_:), then check out this tutorial.

Several types in Swift, such as Array and Dictionary, have functions that accept closures as input. A quick pick:

  • contains(where:) loops over a collection, applies a predicate (a closure) to every item, returns a true if an item satisfies the predicate, otherwise false
  • first(where:) loops over a collection, applies a predicate (a closure) to every item, and returns the item if it satisfies the predicate
  • firstIndex(where:) does the same as first(where:), except that it returns the index instead of the value

You can learn more about these functions in this tutorial. How where is used in Swift is interesting as well, you can learn more about that in this tutorial.

You’ve seen the “map” and “reduce” functions in Swift written as map(_:) and reduce(_:_:) throughout this tutorial. The underscores and colons in those functions are part of the function’s signature, which is a special format to indicate function parameters. For example, the map(_:) function has one unnamed parameter, whereas the reduce(_:_:) function has two. You can learn more about that in this tutorial: Functions in Swift Explained.

Get hired as an iOS developer

Learn to build iOS 14 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Using the Map Function

The map(_:) function loops over every item in a collection, and applies an operation to each element in the collection. It returns a collection of resulting items, to which the operation was applied.

Let’s look at an example. We’ve got an array of temperatures in Celsius that you want transformed to Fahrenheit.

You could use a for-loop, like this:

let celsius = [-5.0, 10.0, 21.0, 33.0, 50.0]
var fahrenheit:[Double] = []

for value in celsius {
    fahrenheit += [value * (9/5) + 32]
}

print(fahrenheit)
// Output: [23.0, 50.0, 69.8, 91.4, 122.0]

The code works fine, but it’s too verbose. 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.

Here’s how we can do the same with the map(_:) function:

let celsius = [-5.0, 10.0, 21.0, 33.0, 50.0]
let fahrenheit = celsius.map { $0 * (9/5) + 32 }
print(fahrenheit)

You could even do all that on one line:

[-5.0, 10.0, 21.0, 33.0, 50.0].map { $0 * (9/5) + 32 }

What happens here?

  1. A constant celsius is defined, an array of doubles, and initialized with a few random Celsius values.
  2. The function map(_:) is called on the celsius array. The function has one argument, a closure, which converts from Celsius to Fahrenheit.
  3. Finally, the result is printed out: the converted array, from Celsius to Fahrenheit.

The map(_:) function transforms one array into another, by applying a function to every item in the array. The closure $0 * (9/5) + 32 takes the input value in Celsius, and returns a value in Fahrenheit. The resulting array of map(_:) is built up out of those converted values.

Let’s take a closer look at the closure. If you’ve worked with closures before, you’ll recognize the short-hand closure syntax. It’s a shorter way to code a closure, by leaving out much of its syntax.

Here’s a less concise alternative:

let celsius = [-5.0, 10.0, 21.0, 33.0, 50.0]

let fahrenheit = celsius.map({ (value: Double) -> Double in
    return value * (9/5) + 32
})

print(fahrenheit)

The actual map(_:) function call, and its closure, is this:

··· = celsius.map({ (value: Double) -> Double in
    return value * (9/5) + 32
})

What’s going on there? The map(_:) function is called on the array celsius. It takes one argument: a closure of type (Double) -> Double.

The first part of the closure, starting with {, indicates that this closure has one parameter value of type Double, and the closure returns a value of type Double. The closure body, starting with return, simply returns the result of the Celsius to Fahrenheit calculation.

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 one Double parameter as input, and are expected to return a Double. Now that you’ve left out the value 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.

Let’s move on to reduce(_:_:)!

Using the Reduce Function

The reduce(_:_:) function loops over every item in a collection, and reduces them to one value. Think of it as combining multiple values into one.

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.

Here’s how:

let values = [3, 4, 5]
let sum = values.reduce(0, +)
print(sum)

The function reduce(_:_:) takes two arguments, an initial value and a closure. In the above code we’re providing the + operator, that’s also a function with two parameters.

You can also provide your own closure, of course:

let values = [7.0, 3.0, 10.0]
let average = values.reduce(0.0) { $0 + $1 } / Double(values.count)
print(average)

In the example above, you’re calculating the average of three numbers. The types of the values in the code are all Double. We’re first adding up all the numbers, and then divide it by the amount of numbers.

The reduce(_:_:) function is different in two ways:

  1. The reduce(_:_:) function has two unnamed parameters; the initial value, and the closure
  2. The closure that’s provided to reduce(_:_:) also has two parameters; the current result of the reduction, and the new value that’s about to get reduced

Here, check this out:

let values = [7, 3, 10]
let sum = values.reduce(0) {
print("\($0) + \($1) = \($0 + $1)")
return $0 + $1
}

print(sum)

In the above example, you can clearly see $0 and $1, the 2 parameters of the closures. When you run the code, this is the output you get:

0 + 7 = 7
7 + 3 = 10
10 + 10 = 20
20

See how we’re starting with 0, then adding 7? In the next step, we’re taking 7 – the current reduction value – and add 3, the “next” value in the reduction.

Here’s another way of looking at it:

0 + 7
(0 + 7) + 3
((0 + 7) + 3) + 10

This also makes it clear why you need an initial value for reduce(_:_:), because that’s the first first parameter of the closure.

Reduction can be tricky to grasp. It’s important to understand that you’re iteratively applying an operation, like +, to a set of values until you’re left with only one value. You literally reduce the amount of values.

Let’s move on to filter(_:)!

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 keeping the values that pass the condition.

Here, check this out:

let values = [11, 13, 14, 6, 17, 21, 33, 22]
let even = values.filter { $0.isMultiple(of: 2) }
print(even)

In the above example, you’re filtering numbers from values that are even. The isMultiple(of:) function returns true when $0 can be divided by 2, and false otherwise.

Unlike map(_:) and reduce(_:_:), the closure filter(_:) needs to return a boolean, so either true or false. When the closure returns true, the value is kept, and when false is returned, the value is omitted. That’s how filter(_:) filters the input array.

Here’s a slightly clearer example, with the closure expanded:

let values = [11, 13, 14, 17, 21, 33, 22]
let even = values.filter({ (value:Int) -> Bool in
    return value.isMultiple(of: 2)
})

print(even) // Output: [11, 13, 17, 21, 33]

In the example, the closure returns a boolean value, indicated with -> Bool. It’s provided one parameter, the item in the collection, and returns the result of isMultiple(of:). Neat!

Combining 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. You know the year each student was born. You want to calculate the combined age of all students born in or after 2000.

Here’s how you do that:

let now = 2020
let years = [1989, 1992, 2003, 1970, 2014, 2001, 2015, 1990, 2000, 1999]
let sum = years.filter({ $0 >= 2000 }).map({ now - $0 }).reduce(0, +)
print(sum)

The above code sample uses chaining. It uses the result of one function as input for another, combining map-reduce-filter. The map() function is called on the result array of the filter() function, and the reduce() function is called on the result of the map() function. Awesome!

The code itself is simple:

  1. Make a constant now and years, and assign a bunch of years to it.
  2. Filter out the years that are below 2000, i.e. keep the ones for which $0 >= 2000 is true
  3. Transform each year into an age, by subtracting the year from now
  4. Add all the ages up, by reducing with +

Let’s take a more interesting example. Check out the following code, taken from the tutorial about FizzBuzz:

let fizzbuzz:(Int) -> String = { i in
switch (i % 3 == 0, i % 5 == 0)
{
case (true, false):
return "Fizz"
case (false, true):
return "Buzz"
case (true, true):
return "FizzBuzz"
default:
return "\(i)"
}
}

let result = Array(2...100).map(fizzbuzz).reduce("1", { $0 + ", " + $1 })
print(result)

See what’s going on? We’re transforming an array with numbers from 2 to 100 into either “Fizz”, “Buzz” or “FizzBuzz” with map(_:), based on the game’s rules. Finally, we’re reducing that array of strings into one big string with reduce(_:_:), combining every value. Neat!

Further Reading

What if you had to code all this with for in loops? You’d use a lot more code. And that’s map-reduce-filter’s power: it’s more concise, often easier to read, and — admit it — pretty damn cool!

Want to learn more? Check out these resources:

Get hired as an iOS developer

Learn to build iOS 14 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Start your iOS career
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy