Promises in Swift
Asynchronous Promises: Proof of Concept in Swift
- 6 min read
Introduction
In this post, we’ll explore building a proof-of concept implementation of Futures and Promises. One of my favorite features of the Swift language is that functions are first-class citizens. Swift’s support for returning functions from functions, accepting functions as function parameters, and Swift’s support for closures let us do some neat things, one of which is building support for Promises. It won’t be a slick as, say, the fantastic JavaScript Q promises library, but it will be a start. Fair warning: some… “artistic liberty” may have been taken with the Promise pattern & general terminology for purposes of demonstration.
Note: I won’t be explaining what promises/futures are; if you’re not familiar, I recommend you look at Introduction to Promises in JavaScript or a few examples from the Q promises library to get a sense for what’s happening. I also recommend checking out Part 1 & Part 2 of “Let’s Make a Framework: Promises” over at DailyJS.
If you recall from my last post on Swift, one thing I would have loved to see was first-class language support for concurrency. Grand Central Dispatch is currently my favorite approach for writing concurrent code in any C-derived language (even over Parallel Extensions in .NET/C#), and to be fair, Objective-C also doesn’t have first-class support for concurrency. But both Swift and Objective-C have building blocks that let us build concurrency abstractions in lieu of language support: closures in Swift, and blocks in Objective-C. Both languages are able to leverage the Grand Central Dispatch library (yes, it’s a library: see libdispatch).
We’ll use Swift’s closures, Swift’s support for first-class functions, and Grand Central Dispatch to build our Promise abstraction.
Target usage of our Promises
So let’s come up with a target to shoot for. Wouldn’t it be great if we could do something like, say, this?
uploadFile().then({
println(“hooray, your file uploaded!”)
}).then({
println("do something else interesting here")
}).fail({
println(“all is lost. accept defeat.”)
})
Our fearless uploadFile() function would bravely — and asynchronously (using Grand Central Dispatch) — transmit our precious file to it’s destination; once the file is uploaded, the then() clauses would be invoked sequentially on the main UI thread. If the file failed to upload, we’d be sad, and we’d let the user know that too by invoking the fail() clause.
Let’s look at how uploadFile() works:
func uploadFile() -> Promise {
let p = Promise.defer()
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
{
let success = self.actualFileUpload()
if !success {
p.reject()
}
dispatch_async(dispatch_get_main_queue(), {
p.resolve()()
})
})
return p
}
func actualFileUpload() -> Bool {
// let's pretend we did real work here
return true
}
- uploadFile() starts by creating deferred Promise object.
- It then puts a task on a GCD background queue. This task uploads the file (left as an exercise for the reader). If the file upload fails, we reject the promise (which will cause execution to fall through to fail() in the first code example).
- Control then transfers back to the main UI thread, where we resolve the promise, which results in our then() or fail() clauses being invoked.
Pretty straightforward. So now the natural question — what does the Promise object look like?
Promise object
We’ll build up the implementation of our Promise object piece by piece. Let’s start at the beginning:
class Promise {
// An array of callbacks (Void -> Void) to iterate
// through at resolve time.
var pending: (() -> ())[] = []
// A callback to invoke in the event of failure.
var fail: (() -> ()) = {}
// A simple way to track rejection.
var rejected: Bool = false
Here’s the base that we’ll build on. We start with:
- An array of pending callbacks, each callback of type Void -> Void. This is what we’ll iterate through and execute sequentially when the promise is resolved. These callbacks map directly to the set of then() clauses used with a Promise.
- A failure callback. This is what is invoked when a promise fails. Note that a default implementation {} is provided — this means users of our Promise object do not need to provide a failure clause if they do not wish to.
- A way to track rejections — a rejected Bool property. If it’s set to true at any point either before our then() clauses are invoked or while they are being invoked, the next then() clause will not execute, and control will pass to the fail() clause.
Now we’ll start adding methods to our Promise class. We’ll start with defer():
class func defer() -> Promise {
return Promise()
}
This method (effectively a static method) simply returns a new Promise object. There is nothing stopping our users from creating a new Promise object directly; this is included to keep with convention.
func resolve() -> (() -> ()) {
func resolve() -> () {
for f in self.pending {
if self.rejected {
fail()
return
}
f()
}
if self.rejected {
fail()
return
}
}
return resolve
}
We’ve said previously that resolve() results in our then() callbacks or our fail() callback being invoked. It’s true, but the truth is subtle here — resolve() doesn’t directly do this. Instead, it returns a function (Void -> Void), and it’s that function’s job to do this. If you look back at our implementation of uploadFile(), you’ll see this:
p.resolve()()
We’re invoking the result of p.resolve(). We could have just as easily said:
let resolveFunction = p.resolve()
resolveFunction()
But, why? It’s true that we could make resolve() invoke our callbacks directly. In our simple uploadFile() example above, that would have worked just fine. But that’s fairly inflexible. In the uploadFile() example, what if we wanted to have the actualFileUpload() function invoke resolve()? Well, to achieve that, we’d have to pass the Promise object around, possibly to parts of our code base that don’t know anything about promises (and that we don’t want to introduce promises to). Instead, this lets us pass around just the resolve function. This way, our Promise abstraction doesn’t need to permeate our codebase.
Moving on, here’s our reject() method:
func reject() -> () {
self.rejected = true
}
Not much of an explanation necessary — we simply set the rejected flag that resolve() looks at.
func then(success: (() -> ())) -> Promise {
self.pending.append(success)
return self
}
This method accepts a single parameter — a callback function (Void -> Void). It simply adds it to our pending callback array, and then — critically — it returns self (the Promise object itself). This is the secret sauce in the Promise recipe that lets us chain callbacks together.
Finally, here’s our fail() method:
func fail(fail: (() -> ())) -> Promise {
self.fail = fail
return self
}
Again, simple and straightforward. It stores our fail() callback for later use (hopefully never), and again, returns a Promise object.
Wrap-up
So there you have it — a basic implementation of the Promise pattern in Swift. Nothing groundbreaking here — we could have done this in Objective-C (e.g., obj-promise, ios-promises, etc…), but I thought it’d be fun to see what this would look like in Swift.
That said, there are a few things this implementation doesn’t do:
- There is no way to pass data from one callback to another. Being strongly typed, Swift makes this a little more difficult than in other languages (e.g., JavaScript) — I haven’t given much thought to this yet, but I suspect Swift’s generics could help here.
- There is no support for a done() clause (but would be quick and easy to implement — hint, it could look a lot like fail()).
- There is no support for concurrency in the Promise object itself. Note that the burden of managing background tasks is on the creator of the promise (e.g., uploadFile()). There is also no support for then() clauses to be backgrounded — they will execute on whatever thread the promise is resolved on.
Finally, I’ve thrown all of the code for this post up on GitHub. Find it here: github.com/rringham/swift-promises (feel free to fork it, send me pull requests, all those great Gitly things). Enjoy, and let me know if you have any questions/suggestions!