Building Reactive Applications With Combine

Pure Functions and Higher-Order Functions

Building Reactive Applications With Combine
Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

In the previous episode, you learned about function types and what it means for Swift to have first-class functions. With these concepts in mind, we continue exploring the functional features of the Swift programming language. In this episode, we explore pure functions and higher-order functions. We start with pure functions.

What Is a Pure Function?

Pure functions are a key concept in functional programming. The idea is surprisingly simple. A pure function is a function that has no side effects. To understand this definition, you need to know what is meant by side effects. We explore side effects in a moment. We can rephrase the definition of a pure function by stating that a pure function only depends on or operates on what it is given. Let me illustrate this with an example.

We define a variable with name a and value 1. The increment(by:) function accepts an argument of type Int and increments the value of a. We print the initial value of a, invoke increment(by:) once, and print the new value of a.

var a = 1

func increment(by value: Int) {
    a += value
}

print(a)

increment(by: 2)

print(a)

The output isn't surprising. The initial value of a is 1 and the new value of a is 3.

1
3

The increment(by:) function is an impure function because it has side effects. What is a side effect? Every time the increment(by:) function is invoked, it modifies the value of a. It modifies state outside its local environment.

Remember the definition of a pure function. A pure function only depends on or operates on what it is given. That is not true for the increment(by:) function because it depends on the value of a.

Let's revise the example and convert increment(by:) from an impure function to a pure function. We can accomplish this by passing the value of a to increment(by:) and returning the result of the addition from the function.

var a = 1

func increment(_ initialValue: Int, by value: Int) -> Int {
    initialValue + value
}

print(a)

a = increment(a, by: 2)

print(a)

Even though the output in the console hasn't changed, we improved the implementation in several respects. The most obvious improvement is that the increment(_:by:) function is reusable because it no longer depends on the value of a. Another significant benefit is that increment(_:by:) no longer has side effects. It operates exclusively on what it is given, the parameters it defines. This has another important benefit. The testability of increment(_:by:) has improved significantly. For a given input, the output is always the same. This is a key feature of pure functions. A pure function always returns the same result for a given set of arguments. In other words, a pure function is predictable and that is a good thing.

Because a pure function doesn't depend on anything but the arguments it is given, a pure function has two interesting characteristics. It always accepts one or more arguments and it always returns a value. If you come across a function that doesn't take any arguments or returns nothing, then you can be sure it is an impure function (or a function that does nothing).

A pure function is predictable and that means it is easy to understand what it does. You don't need to take other variables into account to understand the effect of a pure function. This is a powerful concept and a cornerstone of functional programming.

Are Side Effects Bad?

You may think that pure functions are preferred over impure functions. This isn't accurate. Side effects are necessary if your goal is building software people can use. How would you update the user interface of your application if you could only use pure functions, that is, functions that don't have side effects?

It is more accurate to say that you should only use an impure function if you can't accomplish the same result with a pure function.

What Is a Higher-Order Function?

Let's take it one step further and talk about higher-order functions. Don't let yourself be thrown off by the term higher-order function. Like the term pure function, the idea is simple. A higher-order function accepts one or more functions as arguments or it returns a function. It can also do both. Let me show you an example to make sure you understand the concept.

I bet you are familiar with Swift's map(_:) function. Take a look at this example. We define a variable, fruits, of type [String]. The array of strings contains three elements. I would like to make each string in the array uppercase. The map(_:) function is perfect to carry out this task. We call map(_:) on fruits, passing in a closure. The map(_:) function invokes the closure for each element in the array, passing the element to the closure as an argument. We invoke uppercased() on the String object and implicitly return it from the closure.

import Foundation

let fruits = [
    "apple",
    "orange",
    "cherry"
]

fruits.map { (string) -> String in
    string.uppercased()
}

The result of the map(_:) function is an array of strings. The array of strings that is stored in fruits is not modified. We can simplify the implementation by using shorthand argument syntax and by leveraging type inference.

import Foundation

let fruits = [
    "apple",
    "orange",
    "cherry"
]

fruits.map { $0.uppercased() }

The map(_:) function is a higher-order function because it accepts a function as an argument. In the example, we pass a closure to the map(_:) function, but it can be a function with a name. Take a look at this example.

import Foundation

let fruits = [
    "apple",
    "orange",
    "cherry"
]

func uppercase(_ string: String) -> String {
    string.uppercased()
}

fruits.map(uppercase)

We define a function, uppercase(_:), that accepts an argument of type String and returns a String object. The uppercase(_:) function is a pure function. The signature of the uppercase(_:) function matches what the map(_:) function expects. We can pass the uppercase(_:) function as an argument to the map(_:) function. The result is identical.

What's Next?

Working with functions can be confusing at times. Before you continue, you need to understand what it means for Swift to have first-class functions. We explored the concepts pure, impure, and higher-order functions. We use these concepts when we explore the Combine framework so it is important that you take the time to learn about them.

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy
Next Episode "What Is Combine"