Swift and Pure Functions
Tags: swift, functional
Published: 2022-07-08 14:51:59


I worked on the Salesforce Events iOS team from 2016 to 2020. During this time, we used functional reactive principles to improve our codebase. The benefits that stood out to me the most were:

- Crash rate dropped from one crash in every 100 launches to one in every 40 thousand.
- Bug rate dropped and bug resolution times decreased.
- Increased development velocity.

## What are Pure Functions?

1. A function is pure if it always returns the same value for a given set of inputs.
2. A pure function is free of side effects.  

Side effects are: reads or writes to state outside of the function's set of input params.

Here's a contrived example of an impure function.

```
//not pure
//has side effects
//doesn't always return same values for input
struct Adder {
    var sum: Int = 0
    var left: Int = 0
    var right: Int = 0

    func add () {
        self.sum = self.left + self.right
    }
}
```

And a pure example which meets our two criteria above: returns same output for given input; and is free of side effects.

```
func pureAdd (left: Int, right: Int) → Int {
    return left + right
}
```

## Cost Per Test

Writing tests that assert that a given set of inputs produce the correct output is as straightforward as a test can get. No mocking or state initialization are required.

Given the example functions above, testing pureAdd can be done compactly.

```
assertEqual(pureAdd(1, 99), 100)
assertEqual(pureAdd(1, 0), 1)
assertEqual(pureAdd(2, 2), 4)
```

While the non pure example requires more setup per assertion.
```
let adder = Adder()
adder.left = 1
adder.right = 0
adder.add()
assertEqual(adder.sum, 1)
```

In general, the more side effects a function has, the higher the cost per test

## Test Effectiveness of Pure functions

The goal of unit tests should be to prove that the function under test produces the correct output for the input space.

For the function isTrue(input: Bool) -> Bool the input space has two possibilities: true and false. So, to prove correctness we'll need two assertions:

```
assertEqual(isTrue(true), true)
assertEqual(isTrue(false), false)
```

If we add a second Bool param, we double the input space and double the number of assertions needed to prove correctness:

```
assertEqual(areTrue(true, true), true)
assertEqual(areTrue(true, false), false)
assertEqual(areTrue(false, false), false)
assertEqual(areTrue(false, true), false)
```

For pure functions, the number of assertions needed to prove correctness is a product of the input space. This means that limiting the input space helps lower testing effort for proving correct output for all input.  

To lower the input space we should favor using input parameters with a discrete number of possible values.  For example, consider using enum vs String when possible because an enum has a finite set of values while a String has an unbounded number of possible values.

## Test Effectiveness of Non-Pure functions
For non-pure functions proving correct output for the input space is considerably more challenging for a few reasons:

1. The input into a non-pure function may include object and global state. 
2. This expanded input space increases the number of assertions needed to prove correctness.
3. As seen above, the scaffolding cost per test is often higher for these functions.
4. Shared state can change externally before the function completes.

Given the increased effort required, proving correctness may become intractable for these functions and we instead target code coverage.

While coverage is a good metric to help make sure functions have some level of unit testing, I don't believe having code coverage is equivalent to ensuring correctness of the function.

## Closing Thoughts
Thanks for reading.  This post was focused on how pure functions can benefit us with regard to testing.  However, there are many more benefits to be gained from functional programming and Swift like concurrency, reactive and composition that are worth exploring.