A love letter to reduce

I’ve been using Swift daily for more than a year now, thanks to some pretty cool projects at the company I’m currently working for. Lately I wrote a super simple introduction to enums because so far, being able to think with enums has really been my favorite feature in Swift. Today, I’m adding reduce to that list.

If you look on the internet, this is probably the most common example you’ll see on Swift’s reduce:

let numbers = [1, 2, 3, 4, 5, 6]
let sum = numbers.reduce(0, combine: +) // 21

This is a nice example on how Swift can be short, self-explanatory and, frankly really cool. This is not the most useful use of reduce though.

How reduce works

Reduce takes two arguments: an initial value, and a closure that will be called for every value in the array we are calling reduce on, that’s six times in our example. This closure takes two arguments: the value that was returned by the call to the closure for the previous value and the current value. The first call however (for which we don’t have a previous value) will use the first argument we passed to reduce, here that’s 0.

  1. First call: 0 + 1 = 1
  2. Second call: 1 + 2 = 3
  3. Third call: 3 + 3 = 6
  4. Fourth call: 6 + 4 = 10
  5. Fifth call: 10 + 5 = 15
  6. Sixth (and final) call: 15 + 6 = 21

Now that we’ve showed the idea behind reduce, let’s see a more useful example.

A more useful use of reduce

On this app I’m currently working on, we are not using Interface Builder nor AutoLayout, it’s all manual calculations for the content size of my views. It’s usually pretty straightforward and you’ll end up totaling the heights of all your subviews, including margins, in a for loop. That’s also a great opportunity to explore what reduce can do for you.

let heights = Array(count: numberOfColumns, repeatedValue: CGFloat(0))

let finalHeight = labels
    .enumerate()
    .map { (index: $0.index % self.numberOfColumns, height: $0.element.sizeThatFits(size).height) }
    .reduce(heights, combine: { (array, labelTuple) -> [CGFloat] in
        var heights = array
        heights[labelTuple.index] += labelTuple.height

        return heights
    })
    .maxElement()

Let’s break this code in smaller bits. First, we are creating an array of CGFloat that will contain as much values as we have columns. We are not going to actually use this variable, it will be used as an initial value we call reduce. In our second example, we are turning an array of labels with an infinite number of labels, into an array of CGFloats with 2 values.

For each call to the closure, we will need two things: the label, from which we can get the height and the index of this label in our list, so we can figure out in which column it will end up. That’s why we are calling enumerate() first: for each iteration, we get the index and the value:

for (index, label) in labels.enumerate() {
    print("Index is \(index), Label is\(label)")
}

Enumerate will turn an array of UILabels into an array of (Int, UILabel) tuples:

[(index: 0, element: <UILabel>), (index: 1, element: <UILabel>), (index: 2, element: <UILabel>), ]

On this array, we call map to turn each (index: Int, element: UILabel) into a (index: Int, height: CGFloat) tuple, where height contains the height of the label. Note that we use the modulo operator with the number of columns so that we know at which index we can add the height of our label.

We finally reach the important part here: the call to reduce. Each iteration will give us an array with as much values as we want columns and each time, we’ll add the height of the label to the total of the proper column and return a new array.

Let’s say we have 5 labels of different sizes, before reduce our array would look like this:

[(index: 0, height: 25), (index: 1, height: 35), (index: 0, height: 50), (index: 1, height: 20), (index: 0, height: 40)]

Considering we want 2 columns, we’d have an initial array of 2 values of 0 and each iterations would look that this:

  1. Adding 25 to the total of the 1st column and returning a new array of [25, 0]
  2. Adding 35 to the total of the 2nd column and returning a new array of [25, 35]
  3. Adding 50 to the total of the 1st column and returning a new array of [75, 35]
  4. Adding 20 to the total of the 2nd column and returning a new array of [75, 55]
  5. Adding 40 to the total of the 1st column and returning a new array of [115, 55]

Finally, we use maxElement to retrieve the maximum value in this array: 115. Note that maxElements actually returns an Optional, in case the array you’re calling it on is empty.

Conclusion

I mentionned earlier that I wasn’t using AutoLayout or anything similar. Truth is, in a different situation I’d probably have used a UIStackView. It gave me the opportunity to play with reduce and wrap my head around the concept, which is pretty cool. Reduce is one of those cool concepts coming from functional programming, also known as “fold”. In case you wanted to know nore about it, the Wikipedia page is a good place to start, as well as the official Swift documentation.