HIGHER-ORDER FUNCTIONS IN SWIFT
Higher-order functions is a concept that I found really interesting and took me some time to understand and utilise in a good way in production code. Hopefully I can help you with this process with this post.
Higher-order function is a fancy word for a simple concept: a function that takes a function as an argument, returns a function, or both.
This post will be about two things; how to use some of the built in higher-order functions in the Swift standard library and how you make your own higher-order functions.
CLOSURE AND/OR FUNCTION?
A closure is a self-contained block of code that can be passed around. There is of course more to say about closures but the Swift programming language book describes it very well so I recommend to read about it there if you want to dive deeper.
In Swift functions are first class citizens, this means that Swift supports to pass functions as arguments, return functions and also to assign them to variables. Swift achieves this because a function in Swift is actually a kind of closure.
To put it (very) simple, you can treat functions as closures and closures are blocks of functionality that can capture and store references.
STANDARD LIBRARY FUNCTIONS
You have probably used higher-order functions without really thinking about it as many of the methods in Cocoa Touch utilise them to a great extent. You just need to look at UIViewController, URLSession, or UIView to find common examples of methods that take closures as arguments.
viewController.present(anotherViewController, animated: true) {
/* This is a closure */
}
UIView.animate(withDuration: 3) {
/* This is a closure */
}
But we will now look a little closure at the more functional programming kind of higher-order functions defined in the Swift standard library.
INITIATE AN ARRAY
There is a built-in init method for the array type that makes it easy to initiate an array with the help of a repeated value.
Array(repeating: 1, count: 3) // [1, 1, 1]
But you can also to initiate an array with different values based on context with help of map.
Array(0...3).map { _ in random() } // an example output [3, 543, 13]
WORKING WITH OPTIONALS
Let’s say you have an optional int.
let value: Int? = 1
You want to do some addition on that int.
let newValue = value + 1
// error: value of optional type 'Int?' not unwrapped
This did not work as Int and
Int?
are not the same type. What needs to be done for this to work is to unwrap the Int?
and then do the addition. Let’s try that!let newValue: Int?
if let value = value {
newValue = value + 1
}
That is a lot of code for a very simple thing. It works but it is a bit cumbersome. Map is also implemented on the optional type with the following documentation.
Evaluates the given closure when this Optional instance is not nil, passing the unwrapped value as a parameter.
So let’s see how that works in our favour.
let newValue = value.map { $0 + 1 }
$0
is a shorthand notation for the first input in a closure, $1
for the second and so on. I strongly recommend not using them except for very rare occasions when it is obvious what it represents.
This solution is a lot cleaner and in my opinion as clear or maybe even clearer than the other.
FLATMAP
Flatmap is a lot like map but as the name suggest it also flattens the result. So if you have an arrays nested in another array and you use flatmap the result will be an array with all the combined values of the nested arrays.
[[1], [2]].flatMap { $0 } // [1, 2]
This is of course very useful but flatmap can also be used to flatten away nil values. So if you have an array with optionals and you want to have a new array with non-optionals then flatmap is your friend!
["Cenny", "42", "666", "Seven"].flatMap { Int($0) } // [42, 666]
CLEAR IT UP!
FUNCTIONS
Some of the examples in this post have been a bit ambiguous. Let’s look at an example that squares all values in an array and then try to improve it.
Array(1...5).map { $0 * $0 } // [1, 4, 9, 16, 25]
This is an easy example that can easily be understod by most Swift developers. It may not be obvious for anyone else like a Swift beginner or a developer unfamiliar with Swift. Let’s try and do something about that!
Array(1...5).map { value in value * value } // [1, 4, 9, 16, 25]
We stopped using Swifts shorthand syntax for closure arguments. This makes the code a little more readable but it contains unnecessary implementation details that we don’t care about. So let’s try to remove that and make this code more clear and reusable.
I said that functions are a kind of closure and why that is important is because you can actually pass a function to another function.
Let’s implement a function with a clear descriptive name and put the implementation there.
func square(_ value: Int) -> Int {
return value * value
}
Now we can just tell the map method to use the square function on all the elements in the array.
Array(1...5).map(square) // [1, 4, 9, 16, 25]
So if you compare this version to the one at the start of this section I know which one I prefer.
INITS
Now that we know that you can pass a function or a closure to map (and other higher-order functions too of course) we can solve another problem in a very simple way. If we have an array with one type and we want to transform that to another type, let’s say an array of int to an array of strings one might to something like this.
Array(1...5).map { String(§0) } // ["1", "2", "3", "4", "5"]
This is a fine and okay solution but you can actually reference an init method directly. You can do this cause init methods are basically just type methods and type methods are basically just a function with a scope of that type. So as long as the types of the arguments and what is returned are valid you can just pass in the preferred init method like is shown below.
Array(1...5).map(String.init) // ["1", "2", "3", "4", "5"]
I like this solution very much and you can always extend an exiting type with a custom init method if needed.
If you are using a fallible init (you know the ones that are defined with a question mark and returns an optional) you can use flatMap to filter out any nil value so you only get the values that succeeded.
["www.varvet.com", "www.båten.com"].flatMap(URL.init) // [www.varvet.com]
OPERATORS
Reduce is a very powerful function that is used if you have a collection and want to reduce it into a single value. In the example we have an array of ints and we want to do summation with all the values. Let’s again start with a naive approach.
Array(1...10).reduce(0) { sum, num in sum + num } // 55
This solution is of course an okay solution and simple to understand. There is a lot of noise and repetition in this code for such a simple thing and let’s try to make it simpler.
Array(1...10).reduce(0, +) // 55
Why does this work? It works because operators in Swift are functions and like all functions they have arguments and return values and if these match what you need you can pass them in as a closure/function.
MAKE YOUR OWN HIGHER-ORDER FUNCTIONS
Up to this point we have just used higher-order functions defined in the standard library. Now it is time to go further and make our own higher-order function!
A good starting point is to make simple functions that can help you make your map, filter, reduce, and sorted code even more clear and powerful.
Let’s try and solve a simple problem and start with a familiar solution. Let’s remove all words from an array that are not Cenny or Anna.
["Anna", "Cenny", "Ola"].filter { name in
name == "Cenny" || name == "Anna"
}
// ["Anna", "Cenny]
This solution is short and clear. It is not simple and it does not scale very well. What if we want to add another name that we want to filter out of the array?
Filter is a method that takes a closure with the type
(String) -> Bool
. So let’s make a function that returns a closure with that type.func isWord(_ words: String...) -> (String) -> Bool {
return { input -> Bool in
return words.reduce(false) { didFound, word in
didFound || word == input
}
}
}
This function may seem foreign at a first glance but let it sink in. The first line in the function is a return statement and the beginning of a closure, where the closure type is the same type that the filter function takes as argument. The two next lines are the real implementation where we check if we can find the words in the array and return true every time we find a match.
To use this function we simply call it like any other function and then we pass its return value to the filter function.
["Anna", "Cenny", "Ola"].filter(isWord("Cenny", "Anna"))
// ["Anna", "Cenny"]
See how pretty, scalable, and readable this line of code became. Now we can just add another string separated by a comma and it will work. Compare this to the other solution where we have to add an `or´ operator and then check equality of the string.
What did we do? We created a function that returns a function that we then passed along to another function — pretty neat if I do say so myself!
CHAINING
Now that we know some different higher-order functions let’s put it all together and see how that works. First we will add another function that returns a function. This function will append a string to another string.
func append(_ suffix: String) -> (String) -> String {
return { $0 + suffix }
}
The higher-order functions we have used so far have always returned a result. This means that we can chain them together to make many incremental changes to an object very easy. Let’s filter out some names and then append an exclamation mark using the function we just declared.
["Anna", "Cenny", "Ola"]
.filter(isWord("Cenny", "Anna"))
.map(append("!"))
// ["Anna!", "Cenny!"]
This is a nice way to eliminate the need for temporary variables that would just create unnecessary noise. But heed my warning, more often than not well named temporary variables can be a good thing so use this feature sparsely.
CONCLUSION
If you were not convinced higher-order functions are an amazing thing I hope this blog post showed you how useful they can be in creating clean and clear code. I understand that this is a lot to swallow if you are new to the concept of higher-order functions but stay with it and it will become a very nice tool in your developer brains tool belt.
As with everything, higher-order functions are not a silver bullet but they’re very useful and powerful, so use them wisely.
Source: https://www.varvet.com
Comments
Post a Comment