Closures in API Design

What are closures, and why should we start adding them into our code? Hermés Piqué, founder of Barcelona.IO & Robot Media, addresses these questions and more, as he shares what he has learned from writing closures into APIs.


Closures (0:00)

I love using closures in APIs, and have been using them a lot in RMStore and Haneke. Today I’m going to be sharing some of the things I learned about closures in Swift. But first, let’s go back in time. James Clerk Maxwell was a physicist and mathematician who is actually the father of closure theory. He was also an ornithologist; he studied birds. In 1925, he had to pick a character to denote closures, and inspired by this picture, he picked the curly bracket.

Let’s recap with what closures are. Closures are self-contained blocks of functionality. You might be familiar with this example using animateWithDuration. It takes two closures, one to specify the animations and another one to specify what to do when the animation completes. Closures are great for event callbacks, like animation completed. They’re also very useful to abstract implementation away. The animateWithDuration method doesn’t need to know the specifics of the animations, or the specifics of what is going to happen when the animation finishes.

func showButton() {
    UIView.animateWithDuration(0.5, animations: {
        self.button.alpha = 1
    }, completion: { finished in
        if finished {
            self.button.enabled = true
        }
    })
}

Closures are widely used in Swift, and as such, have a lot of demonstrations for syntax. One of my favorites is the trailing closure that allows you to write the last closure outside of the function call and without using the parameter name.

func showButton() {
    UIView.animateWithDuration(0.5, animations: {
        self.button.alpha = 1
    }) { finished in
        if finished {
            self.button.enabled = true
        }
    }
}

Another important characteristic of closures is that unless told otherwise, they capture the constants and variables used within. That means that in the previous example, self is going to be captured, or retained, until the animation completes. This is made explicit by the compiler, who will complain if we use the self to access instance variables.

Before & After Closures (3:04)

Of course, closures are not strictly needed, but they can make our code better. Let’s look at how we could show an alert with APIs designed before closures, and then with closures. So, our old friend AlertView is rather straightforward. We create a view with the message and title that we need. Then we show it, and we set the delegate to respond to events.

func showQuestionAlert() {
    questionAlert = UIAlertView(title: "Question",
        message: "Pigeons are the new cats?",
        delegate: self,
        cancelButtonTitle: nil,
        otherButtonTitles: "No", "Coo!")
    questionAlert.show()
}

The delegate approach below has a couple of problems. The first problem is requiring to give a reference to the other view if we have more than one other view that we show, and we have different behaviour for each other view. The other thing that I don’t like about this approach is that we need to match buttons with indexes, in order to know which action to perform.

// MARK: UIAlertViewDelegate

func alertView(alertView: UIAlertView, 
    clickedButtonAtIndex buttonIndex: Int) {
    if (alertView === questionAlert) {
        switch buttonIndex { ... }
    }
}

func alertView(alertView: UIAlertView, 
    didDismissWithButtonIndex buttonIndex: Int) {
    if (alertView === questionAlert) {
      questionAlert = nil
    }
}

Luckily, iOS 7 introduced a closure-based API for alert views called UIAlertController. This is how the same functionality looks by leveraging closures and the trailing closure syntax. We create an alert controller. Then we create actions, and each action has a closure that defines how to respond to its selection. Finally, we present the other view with a closure to handle completion. This code has none of the problems of the previous method. What you see here is a result of something called code locality. All the work fits in a single function. This is one of the greatest benefits of closures - code locality.

func showQuestionAlert() {
    let controller = UIAlertController(title: "Question",
        message: "Pigeons are the new cats?",
        preferredStyle: .Alert)
    
    let no = UIAlertAction(title: "No", style: .Default) { _ in 
        ...
    }
    controller.addAction(no)

    let yes = UIAlertAction(title: "Coo!", style: .Default) { _ in 
        ...
    }
    controller.addAction(yes)
    
    self.presentViewController(controller, animated: true) { ... }
}

Using Closures (4:41)

I think BananaKit, referenced in Brian Gesiak’s talk, could use more closures. What if I want to have a custom strategy for peeling the banana? Or, what if I want to wait until the banana is ripe to make banana bread? If you want to add closures to BananaKit, these are some of things we need to think about. So, let’s dig deeper on how to use closures.

@autoclosure (5:07)

We begin with the attribute @autoclosure. To illustrate @autoclosure, think of an inefficient implementation of logical and that evaluates both values. If the right hand value is expensive to procure, then we’re doing more work than we need.

func getExpensiveBool() -> Bool {
    NSThread.sleepForTimeInterval(10.0)
    println("Birds!!!")
    return true
}

let result = and(false, getExpensiveBool())

// prints "Birds!!!"

We can use closures to implement short-circuiting. We modify and to receive a closure that gets a boolean instead of a boolean directly. Then we check the left value, and if the value is false, we simply return false. If the value is not false, and only then, we check the right value. This is great for performance, but is a chore for the users of our API, who will have to wrap the right hand value with a closure every time.

func and(left: Bool, getRight : () -> Bool) -> Bool {
    if !left {
        return false
    }
    return getRight()
}

let result = and(false, { return getExpensiveBool() })

>

Enter @autoclosure. Autoclosure is for parameter values that we might not need to use or evaluate.Using autoclosure is as simple as adding it to the relevant parameter. As soon as you add that, the API user doesn’t need to wrap the right hand value or parameter in a closure anymore.

func and(left: Bool, @autoclosure getRight: () -> Bool) -> Bool

let result = and(false, getExpensiveBool())

>

@autoclosure is extensively used in the Standard Library. The real and uses it. So do assertions, for example, which can be turned off, and thus not have to evaluate some of their arguments.

func &&<T : BooleanType>(lhs: T, @autoclosure rhs: () -> Bool) -> Bool

func assert(@autoclosure condition: () -> Bool, 
            _ message: @autoclosure () -> String = default, 
          file: StaticString = default, 
            line: UWord = default)

@noescape (6:43)

Another attribute of interest is @noescape, which was introduced when Swift 1.2 was released. To present @noescape, imagine that we’re implementing an animation framework. We have a function loop, which is going to take a duration, a reverse see if it goes back and forth, and the closure for the animations it needs to perform. This is how we could use the loop function in a view. We tell the view to scale up and down every half a second. Notice that the closure is capturing self.

func loop(duration: NSTimeInterval, 
          reverse: Bool, 
          animations: () -> Void)

class MYView: UIView  {
    func animate() {
        loop(duration: 0.5, reverse: true) {
            self.scale(1.25)
        }
    }

    func scale(scale: Double) { ... }
}

The question is, how long are we capturing self? That actually depends on the implementation of the loop function. If the loop calls the animations closure every cycle, then we are effectively retaining self forever. But if it only calls animations once, takes snapshots, and then maybe does magic to animate it back and forth, then we are capturing self only for the duration of the function call. Wouldn’t it be great if we could tell the API user our intent with the animations closure?

We can with @noescape. We use it for closure parameters that will not outlive the function call. Adding this attribute is also really easy. You just add it to the parameter that corresponds, and then the compiler will make sure that we use the animations closure accordingly. We don’t even need to use self anymore inside when we are actually providing a closure, because there are no capture risks involved. The compiler lets us get away with it.

func loop(duration: NSTimeInterval, 
          reverse: Bool, 
          @noescape animations: () -> Void)

class MYView: UIView  {
    func doAnimations() {
        loop(duration: 0.5, reverse: true) {
            scale(1.25)
        }
    }

    func scale(scale: Double) { ... }
}

In the Standard Library, @noescape isn’t as widespread as @autoclosure because it was recently introduced. You can start seeing it in sequence functions such as reduce, which uses a closure to reduce a collection into a single value. I think we are going to see it more and more and more.

func reduce<U>(initial: U, 
    combine: @noescape (U, Self.Generator.Element) -> U) -> U

Speaking of @autoclosure, it’s important to mention that @autoclosure implies @noescape. This is done explicitly to limit @autoclosure usage to lazy evaluation. Another thing to consider about @noescape is forward compatibility. Adding @noescape has no negative consequences, but removing it might break user code because @noescape closures can only be passed as @noescape parameters. You should only add @noescape if you’re sure that the function will never call the closure asynchronously. or that you will never change implementation to make that call asynchronously. So, think twice before adding @noescape.

Default Values (9:51)

The last topic is default values for closure parameters. We’ll be using event callbacks as an example. If you are an Objective-C developer, this is super familiar. We fetch an image and we provide a callback for success and a callback for error. In Swift, that method could look like this. Say we don’t care about handling the failure case. With this signature, we’re kind of forced to provide nil for the failure every time.

func fetchImage(success: (UIImage -> Void)?, 
                failure: (NSError -> Void)?)

fetchImage(success: { image -> Void in
    save(image)
}, failure: nil)

Swift has default parameters, and that applies to closures as well. We could simply set the default to nil. This is slightly inconvenient for the API developer, because now we need to check if the closures are nil before calling them. But that’s just an implementation problem, so no big deal.

func fetchImage(success: (UIImage -> Void)? = nil, 
                failure: (NSError -> Void)? = nil) {
    ...
    if let success = success {
        success(image)
    }
}

fetchImage(success: { image -> Void in
    save(image)
})

The big deal is that the behavior of the function is less clear. What does this function do when the closure is nil? We introduce a new possibility. If we want to be explicit, we could remove optionals altogether. Now our fetchImage function expects both closures. The behavior is clear, but the API became really inconvenient to use for the API consumer because they have to provide an empty closure every time. They don’t care about an event or a callback.

func fetchImage(success: UIImage -> Void, 
                failure: NSError -> Void)

fetchImage(success: { image -> Void in
    save(image)
}, failure: { _ in })

Luckily, the default value can also be a closure, and particularly, it can be an empty closure. Now, both implementation and usage of the function are delightful. There is no ambiguity, though the signature might be a bit harder to read. To make the intention of the default value more clear, and simplify the signature, we can set the default to a global function whose name states exactly our intent.

// Empty closure as default
func fetchImage(success: UIImage -> Void = { _ in }, 
                failure: NSError -> Void = { _ in }) {
    ...
    success(image)
}

fetchImage(success: { image -> Void in
    save(image)
})

// Global function as default 
func fetchImage(success: UIImage -> Void = noop
                failure: NSError? -> Void = noop)

Defining noop (12:07)

Using generics and overloading, we can have global a noop function that works for any closure with and without a parameter. We can even make it work for closures that return an optional, and for closures that expect a parameter while also returning an optional.

func noop() {}

func noop<T>(value: T) {}

func noop<T>() -> T? { return nil }

func noop<T, S>(value: T) -> S? { return nil }

But, there is still an issue with our function signature. Remember the trailing closure? Guess which closure are we calling here?

fetchImage() { _ in
    // What closure is this?
}

It’s not the success closure. This means that it’s important to consider the trailing closure when defining parameter order. Taking this into account, we switch the closure parameter order to finally have a function signature we can be proud of.

func fetchImage(failure: NSError -> Void = noop,
                success: UIImage -> Void = noop)

But damn you, Xcode. Xcode is so incompetent at letting developers know about default values. It fails to tell us about them when using code completion, and does a very good job at that.

Xcode

Xcode also doesn’t tell us which is the default value when using Quick Documentation. So, we have to be mindful of how our API looks in code completion and in code documentation, when we write it. For now we can compensate with documentation that specifies the default values for parameters. That, at least, will tell the API consumer what is going on there.

Recap & Coming Next (13:49)

Let’s sum up the most important things we covered today. Closures encourage code locality. We can use the @autoclosure attribute for parameter values that might not be used, and we can use the @noescape attribute for closure parameters that would not outlive the function call. We should prefer no-op closures as default values for closure parameters, unless we have a reason not to. We should consider the trailing closure when defining parameter order. Finally, we have to be mindful about how consumers will discover our APIs.

There are other ways in which we could have defined our fetchImage fuction. I will argue that this code is fluent and a bit nicer to use than the version before. If you like this, then Javi’s talk shows us how to write functions that allow this using Futures.

Regarding the origin of the curly bracket as block delimiters, Internet research tells me that it comes from a language called BCPL, which was created by Martin Richards in 1966. BCPL introduced the curly bracket notation, but in practice used a dollar and parenthesis due to typographical limitations of the time. It then went on to influence and inspire B and C, which in turn influenced most modern languages.



Hermés Piqué

Hermés Piqué