This article is also available in Chinese.
Promises are a way to chain asynchronous tasks. Normally, asynchronous tasks take a callback (or sometimes two, one for success and one for failure), in the form of a block, that is called when the asynchronous operation is completed. To perform more than one asynchronous operation, you have to nest the second one inside the completion block of the first one:
APIClient.fetchCurrentUser(success: { currentUser in
APIClient.fetchFollowers(user: currentUser, success: { followers in
// you now have an array of followers
}, failure: { error in
// handle the error
})
}, failure: { error in
// handle the error
})
Promises are a way of formalizing these completion blocks to make chaining asynchronous processes much easier. If the system knows what success and what failure look like, composing those asynchronous operations becomes much easier. For example, it becomes trivial to write reusable code that can:
- perform a chain of dependent asynchronous operations with one completion block at the end
- perform many independent asynchronous operations simultaneously with one completion block
- race many asynchronous operations and return the value of the first to complete
- retry asynchronous operations
- add a timeout to asynchronous operations
The code sample above, when converted into promises, looks like this:
APIClient.fetchCurrentUser().then({ currentUser in
return APIClient.fetchFollowers(user: currentUser)
}).then({ followers in
// you now have an array of followers
)}.onFailure({ error in
// hooray, a single failure block!
})
(You’ll note that promises are a thing that turns nested/indented code into flat code: a promise is a monad.)
Promises grew to ascendence in the JavaScript community. Because Node.js was designed to have lots of asynchronous capabilities, even simple tasks would require chains of method calls with asynchronous callbacks. This grew unwieldy even after only 3 or 4 of these actions. Promises saved the day, and they’re now part of the official JavaScript ES6 spec. This blog post goes into great detail on how JavaScript’s promises work.
One of the great things about the JavaScript Promise implementation is that there is a very clearly defined spec, called A+, which can be found at promisejs.org. This meant that multiple promise implementations could sprout, and they were fully interoperable, due to JavaScript’s weak type system. As long as your Promise implementation has a then
function that conforms to the spec, it can be chained with promises from other libraries. This is awesome.
While writing the Backchannel API (which was in Node), I grew to love Promises. The A+ spec has a really nice API, eschewing the functional names you would expect on a monad for the simpler, easier-to-understand then
(which is overloaded to act as both flatMap
and map
) . While this API isn’t for everyone, (in particular, I can fully understand why you’d prefer the explicitness of the functional names), I really do like it, and I set out to implement a similar library in Swift.
You can find this library on Github. The process of writing it was enlightening, and I’d like to share some of the things I learned here.
Enums are awesome
Yeah, everyone knows. Enums are great. But because Promises are essentially a state machine, enums are a particularly excellent fit here. The reference implementation of JavaScript’s Promise starts out like this:
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
function Promise() {
// store state which can be PENDING, FULFILLED or REJECTED
var state = PENDING;
// store value or error once FULFILLED or REJECTED
var value = null;
// store success & failure handlers attached by calling .then or .done
var handlers = [];
}
I couldn’t contrive a more perfect example for Swift’s enums if I tried. Here’s the same code in Swift:
enum State<Value> {
case Pending
case Fulfilled(value: Value)
case Rejected(error: ErrorType)
}
final class Promise<Value> {
private var state: State<Value>
private var callbacks: [Callback<Value>] = []
}
The additional data, because it is contingent on the specific state the promise is in, is stored as an associated value on each of the enum cases. Since it doesn’t make any sense for a promise to be in the .Pending
state but have a value, the enum makes that completely inexpressible in the type system.
My only criticism is that generic types can’t be nested inside other types, and this is fixed in Swift 3.
Type systems are nice
When creating a new JavaScript promise, you can use a convenience initializer:
var promise = new Promise(function(resolve, reject) {
someAsyncRequest(function(error, result) {
if (error) {
reject(error);
}
resolve(result);
});
});
You pass it a function that takes two functions: one for if the promise should succeed, and one for if it should fail. For these two functions, order matters. And because JavaScript isn’t type-safe, if you mis-order the functions in the first line above, writing reject, resolve
(which I did more often than I’d like to admit), you can easily pass an error into the resolve
function. Swift’s type-safety, on the other hand, means that the reject
function has the type (ErrorType) -> Void)
and won’t accept your successful result. Gone is the worry that I’ll mess up the order of the reject
and resolve
functions.
Too much types can be frustrating
My Promise
type is generic over Value
, which is the type of the value that comes out of it. This means you can rely on type inference to write code with no types.
let promise = Promise(value: "initialValue") // a fulfilled Promise<String>
Because promises are often chained, relying on inference to figure out what your types will be is especially useful. Having to add explicit types to each step in the chain would be very frustrating, and ultimately not particularly Swift-like.
My first crack at this was generic over Error
as well. This strictness meant that creating a fulfilled promise required you to specify your error’s type up-front, every time.
let promise = Promise<String, APIError>(value: "initialValue")
This adds a lot of unnecessary baggage to what used to be a simple line of code, so I remove the ability to specify what the type of the error was.
Unfortunately, removing explicit error types means that there’s one small type-system goodie that I have to miss out on. If you make an empty enum called NoError
, it effectively expresses that the promise can’t fail. Since empty enums can’t be initialized, there’s no way to make the promise enter the rejected state. This is a sad loss, but ultimately, I decided it was worth it, since it made using promises in every other context way simpler. I hope using the class in practice will give me the insight into whether this was a good decision or not.
Relatedly, Swift’s generics manifesto includes “default generic arguments”, which would be a great way to get around this problem: you’d be able to say the default is ErrorType
, and if anyone wants to get more specific, they have that capacity.
Functional methods are hard to grok
The promise type is a monad, meaning you can call flatMap
on it. The function that you pass into flatMap
returns a new promise, and that promise’s state becomes the state of the chain.
The name of the flatMap
function is completely inscrutable, though. It doesn’t express what’s actually happening here, in an easy-to-read way. This is part of the reason I prefer A+’s Promise API. The then
function in JavaScript overloaded to act as both flatMap
(returning a new promise for the chain) and map
(returning a new value for the next promise in the chain). then
now just means “do this thing next” without respect to exactly how the next thing works.
Since Swift’s type system knows when functions return Void
, then
can also be overloaded to accept functions that don’t return anything, to allow you to “tap” into the chain. I’ll know more when I use this class in a project, but I look forward to seeing if this “tap” version of then
is useful. A triple overload might be too much, but I think it’ll be nice to be able to use then
without having to think about the precise functional term for what I’m trying to do.
Tests are good
Once I wrote a basic implementation of the class, I wrote a few tests. I got some experience with XCTest’s expectationWithDescription
and waitForExpectationsWithTimeout
, which are pretty nice APIs to work with.
Like the cookbook project, having a full suite of tests for the Promise
class was extremely useful. As always, there was some up front cost in writing the tests, but it was totally worth it. While I was refactoring and cleaning up this code, the tests caught numerous errors. A promise implementation is very fickle, and tiny details about the order that code is executed change the behavior of the class in subtle ways. Having a test suite that confirms that a refactor is truly isomorphic is great.
Threading is hard
Because it inherently deals with threading and asynchronicity, Promise
needs to be a thread-safe class. To make the class thread-safe, its instance variables need to be accessed all from the same queue. This was harder than I expected it to be. Even after I’d thought I’d done it right, there were still a few places where I’d screwed it up.
Two of the tests in particular were very flaky and would fail every 5-10 times I ran the test suite. There’s nothing scarier than a flaky test, because it’s so easy to just assume it was a cosmic ray that hit your computer’s RAM at the exact right moment to cause your test to fail.
One of the flaky tests was resulting in an EXC_BAD_ACCESS
, which was very confusing, because I couldn’t think of a way in Swift that I would be accessing bad memory. It took me a while, but I got finally got a log message that suggested it was a threading issue. I was appending things to an array from multiple threads simultaneously. I corrected the code that accesses the instance variables to use the dispatch queue correctly, and the flaky tests became reliable.
You can find the code on Github. I haven’t made it a full library yet, with public
declarations and a podspec yet. I want to see what it’s like to use it in a real app first.
Promises seem complex and magical, but the implementation flows almost naturally from the types that each version of then
has. Once I had an implementation that worked, I could write tests against it, and those tests enabled me slowly refactor my code and find edge case bugs.