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 NSAttributedString
s, 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. 🤓