Swift Basics: reduce()

This post is also available as an interactive Swift Playground. 🤓

The reduce function is super useful for when you need to compute (or reduce 😏) a set of values down to a single value.

Let's take a look at a simple example. Say we wish to sum an array of numbers to find the total. Without the reduce function, we might use a foreach loop:

    let valuesToReduce: [Int] = [1,2,3,4]
    var total: Int = 0
    
    valuesToReduce.forEach { value in
        total += value
    }
    
    total // total: 10

However, in Swift we try to avoid mutability.

Here is the same calculation written using reduce. Take a look then we'll break it down:

    // use `reduce` to sum each Int in an array, resulting an Int:
    let total = valuesToReduce.reduce(0) {
        (accumulation: Int, nextValue: Int) -> Int in
        return accumulation + nextValue
    }
    
    total // 10

or written more simply as:

    let total = valuesToReduce.reduce(0) { $0 + $1}
    total // 10

Note that the closure we to reduce is called once for every value of the array.

The zero that we pass in as a parameter is called the accumulator, that is: the starting value upon which successive calls of reduce function will be applied, effectively accumulating our final result. Actually, given that we're using value types and 0 is immutable, we will not be mutating the zero - we're really passing the return value from the first call (i.e. 0 + 1 = 1 - a new value) back in as the starting value (accumulator) of the next call.

We will see at the end that we can also use a mutable object as the accumulator and literally build up our object with each successive pass - which is sometimes useful for working with Cocoa.

Creating An Array from an Array:

The output of reduce can really be anything - even another Array. As a second example, let's use reduce to create a handy deduplication function which we can add as an extension on Array type.

The first thing to decide is what our starting value for this shall be. Our algorithm will be as follows: "start with an empty array and append each item, so long as that item isn't already present in the array". By this logic we shall finish with an array of distinct unique values.

Thus, we should start with an empty array of type <Element> (the Generic placeholder for the type of value the array can hold): [Element]():

    extension Array where Element: Equatable{
        func deduplicate() -> [Element]{
            return reduce([Element]()) { (accumulation: [Element], find: Element) -> [Element] in
                guard accumulation.indexOf(find) == nil else {return accumulation}
                return accumulation + [find]
            }
        }
    }
    
    let result = ["Amsterdam", "Berlin", "Paris", "Amsterdam"].deduplicate() // -> ["Amsterdam", "Berlin", "Paris"]

Whoa whoa, slow down:

So:

extension Array where Element: Equatable - we should constrain this extension to be available only when the Array holds values that are equatable. Otherwise, how could we identify duplicates?

func deduplicate() -> [Element]{ - we're returning an Array of the elements of the same type that the starting array holds.

reduce([Element]()) { (accumulation: [Element], find: Element) -> [Element] in - [Element]() is what we start with - an empty array. This is the Accumulator that we'll be using. The signature of the closure we're passing to reduce has the accumulation that we're building up accumulation: [Element], and find: Element, the current element that we want to find.

guard accumulation.indexOf(find) == nil else {return accumulation} - if the element we wish to find is already in the accumulation, then exit early from this iteration, returning an unchanged accumulation value. This is the escense of deduplication.

return accumulation + [find] - otherwise, return the accumulation with find appended.

In the next example, we'll show that reduce can also be called on a Dictionary type, and that the accumulator can also be mutable if need-be:

Using a Mutable accumulator instead:

Say we want to take a Dictionary of type [String : NSURL], that is, a mapping of Town Name to Map URL, and turn it into an NSAttributedString of tappable (once added to a UILabel) linked text.

i.e. this:

    let placeURLPairs: [String : NSURL] = [
        "Amsterdam" : NSURL(string: "https://www.google.nl/maps/place/Amsterdam/")!,
        "Newcastle" : NSURL(string: "https://www.google.nl/maps/place/Newcastle/")!,
        "Manchester" : NSURL(string: "https://www.google.nl/maps/place/Manchester/")!
    ]

to this:

We can, naturally, use a reduce function to acheive this. However, the API for building up NSAttributedStrings, is quite suited to using an NSMutableAttributedString and applying successive attributes and String values on it.

It's easy to tweak our reduce function to, instead of returning a new value each time, instead return a mutated version of the passed in accumulator, so this same object will be passed into each iteration and then will be the result by the end:

    func attributedString(places placeURLPairs: [String: NSURL]) -> NSAttributedString{
    
        return placeURLPairs.reduce(NSMutableAttributedString()) {
            (accumulator: NSMutableAttributedString, placeURLPairs: (String, NSURL)) -> NSMutableAttributedString in
    
            // unpack the name and the URL:
            let (placeName, url) = placeURLPairs
    
            // Create the local attributed String which we'll append
            let localAttributedString = NSMutableAttributedString(string: placeName + " ")
    
            // Add a link to this name
            localAttributedString.addAttribute(
                NSLinkAttributeName, value: url, range: NSRange(location: 0, length: placeName.characters.count)
            )
    
            // Add this attributed string onto the mutable Accumulator
            accumulator.appendAttributedString(localAttributedString)
    
    		// Pass the accumulator back out again
            return accumulator
        }
    }
    
    
    let placeURLPairs: [String : NSURL] = [
        "Amsterdam" : NSURL(string: "https://www.google.nl/maps/place/Amsterdam/")!,
        "Newcastle" : NSURL(string: "https://www.google.nl/maps/place/Newcastle/")!,
        "Manchester" : NSURL(string: "https://www.google.nl/maps/place/Manchester/")!
    ]
    
    let attributedLinkedTownsString = attributedString(places: placeURLPairs)
    
    let label = UILabel(frame: CGRect(x: 0, y: 0, width: 1000, height: 200))
    label.attributedText = attributedLinkedTownsString

Hopefully this was useful, do let me know in the comments if there's any feedback or comments.

This post is also available as an interactive Swift Playground. 🤓