First class functions in Swift
Languages that support first class functions enable you to use functions and methods just like any other object or value. You can pass them as arguments, save them in properties or return them from another function. In order words, the language treats functions as "first class citizens".
While Swift is hardly the first language to support this way of handling functions, it's normally a feature you see in more dynamic languages like JavaScript or Lua. So combining Swift's robust static type system with first class functions becomes a quite interesting combination, and can allow us to do some pretty creative things π.
This week, let's take a look at a few different ways that first class functions can be used in Swift!
Passing functions as arguments
Let's start with the basics. Since functions can be used as values, it means that we can pass them as arguments. For example, let's say that we want to add an array of subviews to a view. Normally, we might do something like this:
let subviews = [button, label, imageView]
subviews.forEach { subview in
view.addSubview(subview)
}
The above code works, and there's nothing wrong with it. But if we take advantage of first class functions, we can actually reduce its verbosity quite a lot.
What we can do is treat the addSubview
method as a closure of type (UIView) -> Void
(since it accepts a view to add, and doesn't return anything). This perfectly matches the type of argument that forEach
accepts (a closure of type (Element) -> Void
, and in this case the Element
type is UIView
). The result is that we can pass view.addSubview
directly as an argument to our forEach
call, like this:
subviews.forEach(view.addSubview)
That's pretty cool! π However, one thing to keep in mind, is that when using instance methods as closures like this you are automatically retaining the instance as long as you are retaining the closure. This is not a problem at all when passing a function as a non-escaping closure argument like above, but for escaping closures it's something to be aware of in order to avoid retain cycles.
For more information about escaping vs non-escaping closure arguments and capturing, check out "Capturing objects in Swift closures".
Passing initializers as arguments
The cool thing is that it's not only functions and methods that can be used as first class functions in Swift - you can also use initializers this way.
For example, let's say that we have an array of images that we'd like to create image views for, and that we want to add each of those image views to a stack view. Using first class functions we can achieve all of the above using a simple chain of map
and forEach
:
images.map(UIImageView.init)
.forEach(stackView.addArrangedSubview)
What I like about structuring code this way is that it becomes very declarative. Instead of nested for
loops we are simply declaring what we want the outcome to be. There's of course a balance to be struck between declarative, compact code and readability, but for simple operations like the above I think taking advantage of first class functions can be super nice.
You can find more examples and use cases for passing initializers as closures in "Simple Swift dependency injection with functions" and "Time traveling in Swift unit tests".
Creating instance method references
Let's dive a bit deeper into the wonderful world of first class functions π. One thing that was puzzling me for the longest time was the fact that I got instance method auto completion suggestions when I wanted to call a static method. Try typing UIView.
in Xcode to see what I mean, you get every instance method as a suggestion π€.
At first I thought this was an Xcode bug, but then I decided to investigate it. It turns out that for each instance method a type has, there's a corresponding static method that lets you retrieve that instance method as a closure, by passing an instance as an argument.
For example, we can use the following to retrieve a reference to the removeFromSuperview
method for a given UIView
instance:
let closure = UIView.removeFromSuperview(view)
Calling the above closure would be exactly the same as calling view.removeFromSuperview()
, which is interesting, but is it really useful? Let's take a look at a few scenarios where using this feature can actually lead to some pretty cool results.
XCTest on Linux
One way that one of Apple's frameworks uses this feature is when running tests using XCTest on Linux. On Apple's own platforms, XCTest works by using the Objective-C runtime to look up all test methods for a given test case, and then runs them automatically. However, on Linux there's no Objective-C runtime, so it requires us to write a bit of boilerplate to make our tests run.
First, we have to declare a static allTests
dictionary that contains a mapping between our test names and the actual methods to run:
extension UserManagerTests {
static var allTests = [
("testLoggingIn", testLoggingIn),
("testLoggingOut", testLoggingOut),
("testUserPermissions", testUserPermissions)
]
}
We then pass the above dictionary to the XCTMain
function to run our tests:
XCTMain([
testCase(UserManagerTests.allTests),
])
Under the hood, this is using the feature of being able to extract instance methods using its static equivalent, which enables us to simply refer to the functions by name in a static context, while still enabling the framework to generate instance methods to run. Pretty clever! π
Without this feature, we would've had to write something like this:
extension UserManagerTests {
static var allTests = [
("testLoggingIn", { $0.testLoggingIn() }),
("testLoggingOut", { $0.testLoggingOut() }),
("testUserPermissions", { $0.testUserPermissions() })
]
}
Calling an instance method on each element in a sequence
Let's take this feature for a spin ourselves. Just like we were able to pass another object's instance method as an argument to forEach
, wouldn't it be cool if we could also pass an instance method that we want every element in a sequence to perform?
For example, let's say that we have an array of subviews that we want to remove from their superview. Instead of having to do this:
for view in views {
view.removeFromSuperview()
}
Wouldn't it be cool if we could do this instead:
views.forEach(UIView.removeFromSuperview)
The good news is that we can, all we have to do is to create a small extension on Sequence
that accepts one of these statically referenced instance methods. Since they are functions that generate a function (Functionception! π) their type will always be (Type) -> (Input) -> Output
, so for our extension we can create a forEach
overload that accepts a closure of such type:
extension Sequence {
func forEach(_ closure: (Element) -> () -> Void) {
for element in self {
// Get an instance method for the element by calling 'closure'
// and then run it directly using ().
closure(element)()
}
}
}
We can now easily call instance methods on each member of any sequence! π
Implementing target/action without Objective-C
Let's take a look at one more example. In UIKit, the target/action pattern is very common, for everything from observing button clicks to responding to gestures. I personally really like this pattern, since it lets us easily use an instance method as a callback without having to worry about the retain cycle problem we discussed earlier (when referencing an instance method as a closure).
However, the way target/action is implemented in UIKit relies on Objective-C selectors (which is why you have to annotate private action methods with @objc
). Let's say we wanted to add the target/action pattern to one of our custom views, and let's say we want to do it without relying on Objective-C selectors. That might sound like a lot of work and that it will make things awfully complicated, but thanks to first class functions - it's quite simple! π
Let's start by defining an Action
typealias as a static function that returns an instance method for a given type and input:
typealias Action<Type, Input> = (Type) -> (Input) -> Void
Next, let's create our view. We'll create a ColorPicker
that lets the user pick a color in a drawing app, and add a method for adding a target & action to it. We'll keep track of all observations as closures, and every time a closure is run we generate an instance method for the given target and run it, like this:
class ColorPicker: UIView {
private(set) var selectedColor = UIColor.black
private var observations = [(ColorPicker) -> Void]()
func addTarget<T: AnyObject>(_ target: T,
action: @escaping Action<T, ColorPicker>) {
// We take care of the weak/strong dance for the target, making the API
// much easier to use and removes the danger of causing retain cycles
observations.append { [weak target] view in
guard let target = target else {
return
}
// Generate an instance method using the action closure and call it
action(target)(view)
}
}
}
The cool thing is that we can actually use first class functions even more above. By using the map
API on Optional
, we can generate the instance method and call it in one go, like this:
observations.append { [weak target] view in
target.map(action)?(view)
}
Finally, let's use our new target/action API in a CanvasViewController
, which will present our ColorPicker
. Just like we would add a target & action to a UIButton
or UIGestureRecognizer
, we can simply pass the view controller itself and an instance method to run, like this:
class CanvasViewController: UIViewController {
private var drawingColor = UIColor.black
func presentColorPicker() {
let picker = ColorPicker()
picker.addTarget(self, action: CanvasViewController.colorPicked)
view.addSubview(picker)
}
private func colorPicked(using picker: ColorPicker) {
drawingColor = picker.selectedColor
}
}
Type safe targets & actions without any Objective-C selectors or risks for memory leaks, using just a few lines of code - pretty cool! π
Conclusion
First class functions is a very powerful feature. By being able to use functions and methods in a much more dynamic way we can achieve some pretty interesting results, and it can be really useful when implementing certain kinds of APIs.
However, in the famous words of Uncle Ben; with great power comes great responsibility. While I think it's super useful to learn about these kinds of features and how they work, it's also important to exercise some restraint when using them. Our goal should always be to create APIs that are nice and easy to use, and to write code that is both easy to read & maintain. First class functions can definitely help us serve that goal, but if taken too far it can also lead to quite the opposite. As always, my recommendation is to experiment, try these features out and see for yourself if and how they can be used in your own code.
What do you think? Have you used these kind of first class function features before, or is it something you'll try out? Let me know, along with any questions, comments or feedback you have - either via email, or on Twitter @johnsundell.
Thanks for reading! π