Swift — Higher order functions.
Swift, as a multi paradigm programming language, provides a wide range of convenient functions allowing us to write declarative and self-explanatory code in a concise way that makes our code easier to read and understand. This is referred as functional programming.
map(), filter(), reduce() are some of these functions.
Behind the curtain.
Functions, in Swift, are first-class citizens. They can be treated as any other object that is be assigned to variables or passed around as function arguments.
— The map() case.
Let’s take a look at the actual implementation of map() in the Sequence.swift file of the standard library.
@inlinable
public func map<T>(
_ transform: (Element) throws -> T
) rethrows -> [T] { let initialCapacity = underestimatedCount
var result = ContiguousArray<T>()
result.reserveCapacity(initialCapacity) var iterator = self.makeIterator() // Add elements up to the initial capacity without checking for
regrowth.
for _ in 0..<initialCapacity {
result.append(try transform(iterator.next()!))
} // Add remaining elements, if any.
while let element = iterator.next() {
result.append(try transform(element))
} return Array(result)}
map() takes a throwing function with a generic type ‘Element‘ as a parameter and returns some other generic type ‘T‘.
The first generic type Element refers to the Sequence elements type (map being part of the Sequence extension) that is your initial object without transformation.
The second generic type T refers to the new type after your object has been transformed.
A throwing function passed in as an argument must somehow be handled by the function using it, hence the ‘rethrows‘ keyword.
‘Rethrows‘ is used when one of the function argument throws as well.
A ‘do, try, catch‘ block won’t be needed as long as the function argument doesn’t throw. This will be assessed at compile time.
map() will return an array of the transformed objects.
Within the function’s body, a ContiguousArray is used to store the Sequence elements consecutively in memory. It optimizes storage for class or @objc protocol type.
A reserved capacity with an underestimated count is set on the array. Some computation can be expensive and the size of the array may depend upon that. This provide some guarantees that everything below this count will be in the sequence.
An iterator is provided (using the next() method from the IteratorProtocol) to go through each element from the current sequence in order for the transformation to be applied to each element. The result, if the transformation succeed, is appended to the contiguous array.
An array of transformed objects will at last be returned.
Usage in code.
- map()
[1, 2, 3, 4].map { $0 * 2 }// output: [2, 4, 6, 8]
If you try to apply a failable transformation, you might end up with an array of optional objects.
[“1”, “2”, “Hello world”].map { Int($0) }// output: [Optional(1), Optional(2), nil]
This is pretty much useless in our code right now, fortunately there is a map variant called compactMap(). This will create an array with non optional objects.
- compactMap()
[“1”, “2”, “Hello world”].compactMap { Int($0) }// output: [1, 2]
compactMap() becomes also very handy when it comes to objects casting providing some safety that you array object will only contain the casted values, if the cast succeed.
- filter()
[1, 2, 3, 4, 6].filter { $0.isMultiple(of: 2) }// output: [2, 4, 6]
- reduce()
[1, 2, 3, 4, 6].reduce(0, +)// output: 16
The beauty of functional programming is that one could chain all of these operations to a specific object while keeping a neat and concise code.
[“1”, “2”, “3”, “4”, “Hello world”]
.compactMap { Int($0) }
.filter { $0 <= 3 }
.reduce(0, +)// output: 6
The importance of being lazy.
« A sequence containing the same elements as this sequence, but on which some operations, such as map and filter, are implemented lazily. »
let evenNumbers = [1, 2, 3, 4, 5]
.map { $0 * 2 }
.filter { $0.isMultiple(of: 2 }let firstEvenNumber = evenNumbers[0]// output: 2
The current implementation of evenNumbers induces a loop through each value from the array for each operation.
While firstEvenNumber only cares about accessing and storing the first value from evenNumbers, the latter had to apply the operations to all the stored values from the array which definitely leads to some unnecessary extra work.
This is where ‘lazy‘ kicks in. Its underlying type is a LazySequence.
No upfront work will be done until it is actually needed, in other words when trying to retrieve the first value of evenNumbers, it will only map and filter the first element from the array, no more.
let evenNumbers = [1, 2, 3, 4, 5]
.lazy
.map { $0 * 2 }
.filter { $0.isMultiple(of: 2) }let firstNumber = evenNumbers[0]
This is a very powerful keyword, performance wise, when dealing with large arrays.
Conclusion.
Using higher order functions is pretty straightforward and turns out to be very convenient when dealing with consecutive operations on objects. This declarative approach improves code readability while providing some quick understanding regarding the developer intents. Dealing with large arrays, on which some expensive computation may be necessary, can lead to performance issues, the lazy keyword allows some optimization avoiding all of the unnecessary work.
Thanks for reading, stay tuned for upcoming articles ;)