Promise Internals: how to build an A+ compliant Promises library

Asynchronous programming in Objective-C was never been a truly exciting experience.
We have used delegates for years (I can still remember the first time I’ve seen it, it was around 2001 and I was having fun with Cocoa on my Mac OS X) and not so long ago we have also joined the party of completion handlers.
However both of these processes does not scale well and does not provide a solid error handling mechanism, especially due to some limitations of the language itself (yeah you can do practically anything in C but this is out of the scope of this article).

It’s damn easy to lost yourself in a callback pyramid of doom (also known as callback hell) and generally your code ends up being not so elegant and not so straightforward to read and maintain.

Promises can help us to write better code and, with the help of constructs like await/async it’s really a joy to deal with asynchronous programming.
Back in November 2016 I’ve decided to work on a Promise library just to learn more about how this concept is implemented and how can I do it with a modern language like Swift.
In this article I would to give a deeper look inside the architecture of my Promise library: Hydra.
In this article you will not learn about how to use Hydra in your next killer app but you will learn how it works behind the scenes (however I’ve wrote a complete documentation for Hydra and it’s available on GitHub).

What’s a Promise?

A promise is an object that may produce a single value sometime in the future; this value can be the object you are expecting for (ie. a JSON response) or the reason of failure (ie. a networking error).
A promise may be in one of the following states: resolved (or fulfilled), rejected or pending. A promise starts in pending state and can transit to another of the two states; once settled it cannot be resettled.

Promise’s users can attach callbacks (or observers) to get notified about any state change. The most common operators for a Promise are then and catch, used to get the value of a promise or catch any occurred error. However there are several other operators which simplify a lot how networking code is written, but we’ll look at them later.

A little bit of history

The history of Promise starts a long time ago, in early 1980’s; first implementations began to appear in languages such as Prolog and Lisp as early as the 1980’s. The word “Promise” was conied by Barbara Liskov and Liuba Shrira in an academic paper called “Promises: linguistic support for efficient asynchronous procedure calls in distributed systems” (1988).

As promise interest grew, a new specification for Promise was redcated by the ECMAScript standard: Promise/A+ was written to define the boundaries and behaviour of a Promise.

Main rules for a compliant Promise/A+ implementation are:

  • A promise or “thenable” is an object that supplies a standard copliant then function.
  • A pending promise may transition into a fulfilled or rejected state.
  • A fulfilled or rejected is settled, and must not transition into any other state.
  • Once a promise is settled, it must have a value. This value must not change.

Promise Class Internals

Due to the type-safe nature of Swift, it’s easy to think to a promise which can return a well defined type of output; with generics we can easily specify what kind of object we are expecting on promise’s settle.

Promise can be initialized in two different ways:

  • in pending state along with a context and a body. The body of a promise define the async action you want to accomplish; the contextallows you to set a Grand Central Dispatch queue in which the body is executed.
  • in a settled state ( resolved or rejected) along with a value or an error. Generally you don’t need to init a settled promise but it’s useful to implements specific behaviour for some custom operators (we’ll look at this later).

The first case is fairly more interesting to look.
First of all: a pending promise is not resolved immediately just after user initialize a new instance but in a lazy way; it simply retain a reference to the body closure and the context received.
The body closure will be executed only when you attach an operator to the instance (while lots of implementations avoid lazy running Hydra fully supports it).

In its simplest case you may want to get notified when your async operation resolves successfully (with the object instance you are expecting for) or fail with an error.
This can be done attaching then & catch operators:

This promise is defined with Int as expected result. It also execute the body of the promise in background GCD queue. Both then and catch closures are executed in main thread because does not specify a custom context as parameter.

But how it works?
This is a snippet of the Promise class in Hydra:

As you can see we define the following properties:

  • state: define the current state of the Promise; it’s basically an enum with the following cases: resolved(_:Value), rejected(_Error) and pending (the state also encapsulate the result, value or error, of the operation).
  • stateQueue: this is a GCD internal queue used to keep Promise class thread safe: as we said Promise cannot change from a settled state. Any change to state property must be done synchronously and this queue is used to ensure this binding.
  • body: this is a reference to the closure with the async code we want to execute.
  • context: GCD queue in which the body will be executed.
  • observers: this is an array of registered closures used to receive notifications about any change in Promise’s current state. Observer<Value> is an enum with two types: first is used to get notification about fulfill events ( .onResolve(ctx: Context, body: (Value->Void))); the other is used for rejection ( .onReject(ctx: Context, body: (Error -> Void))). Operators register obsever to get notified about promise’s events; each observer’s body is executed into specified context.
  • bodyCalled: we need to ensure Promise’s body is called once and once time only. As with state, also this property is set synchronously using stateQueue.

The signature of the body exposes two input arguments ( ...{ resolve, reject in...`); when async code return a value or throws an error it must be signal it to the parent Promise by calling one of these functions: once done promise did change the internal state and call any related registered observer (ie. if fulfilled only onResolve observer will be called, in case of rejections only observers of type onReject are called).

This is the snippet of code which is responsible to execute the body of the promise:

As we said runBody() can be executed one time only (and only if the promise is pending): we can ensure it using the stateQueue‘s sync{} call.
Just after that we can asynchronously call the body; as you can see it’s encapsulated in a do/try statement: this because the body closure is throwable; this is not required but it’s a nice addition used to reject a promise without calling reject func but in a more Swifty way.

body’s closure ends with a resolve(value: Value or reject(err: Error); based upon the result the Promise itself change its state to resolved or rejected via self.set(state:) func.
This is how self.set(state:) is implemented:

As we said Promise’s state change event must be executed synchronously and only if current state is pending.
The next step after setting the state is to iterate over all interested observer and notify them about the good news.

The same iteration must be done also after a new observer is added to the queue (it’s implemented in the same way so we don’t look at it here).
This is the basic architecture of the Promise: in the next chapter we’ll look about how some interested operators are implemented.

Inside (some) interesting operators

It’s time to look at how the operators are implemented. For obvious reasons we cannot see all the operators available in Hydra but only a specified interesting subset (you can however get a deep look at the code because it’s pretty well documented).

Before starting these are two important definitions:

  • sourcePromise is the promise on the left side of the operator
  • nextPromise is the promise returned by the operator as the result of its transformation (if any).

.then()

In its simplest form then is used to resolve a chain and get the value if it fullfill: it simply defines a closure you can execute for this event but not transformations of the Promise’s output are allowed.
Get a look at implementation:

As you can see the behaviour is pretty straightforward. nextPromise is returned as the output and it has the same type of sourcePromise.

First of all we need to watch the result of sourcePromise inside nextPromise‘s body: this is done by calling nextPromise.runBody() and adding observers via self.add(...).

The next step is to resolve sourcePromise (by calling self.runBody()):

  • if successful, body is executed and it may have the opportunity to reject the chain or accept it (this is the reason for a throwable body).
  • If fails body is skipped and the error is simply forwarded to the nextPromise.

@discardableResult in signature is necessary to silent the compiler while you can safely ignore nextPromise as output of the operator.
contextparameter is optional and if not specified we’ll use the main threadto execute the body closure.

then() to chain with another promise by passing its first argument

Another use of then is to resolve sourcePromise with a value, then pass it as first argument of another promise returned by the body closure.
Basically it allows you to do:

myAsyncFunc1().then(myAsyncFunc2)

(myAsyncFunc2 must accept only one parameter without the explicit label)

Implementation is pretty similar to the previous one: the big difference is inside the onResolve observer. In this case we expect a Promise as output for body; chainedPromise must be also resolved by passing the value obtained by sourcePromise as argument while the final result will be forwarded to the nextPromise.
According to it the output of this operator is Promise which takes the result of sourcePromise, (optionally) transform it to another type and execute another promise defined into the body.

.catch()

catch is another fundamental operator: it’s used to handle the rejection of a sourcePromise.
Take a look at the implementation:

Concept is very similar to then but in this case we are interested in in handling the rejected state.
While onResolve implementation simply forward the result to the nextPromise and along the chain, onReject must execute catch‘s body; as like we’ve seen with then even this may reject the chain (in fact it’s not a real reject because the chain was already rejected, we can call it a change in output error).

.retry()

retry allows you to repeat a failed promise for a number of specified attempts; you can, for example, use it to repeat network connection attempts or failable operations.
The implementation of this operator introduce a dirty secret: at the beginning we have said which a settled Promise cannot be unsettled; however, in order to make a coincise implementation we have added an internal method which allows us to reset the state of a Promise and re-execute it.

resetState() scope is to set the state to pending and allows runBody() to be executed once time again. Both these operation must be done by preserving thread-safe binding, so we will encapsulate it in a sync session of stateQueue.

By default all promises will be executed in a background parallel queue ( allPromiseContext); output of the operator is a promise — called allPromise — which will be resolved when all input promises succeded or at least one fails.
allPromise has as output an array which, once resolved, contains a list of resolved values in the same order of the input promises.

The first step is to iterate over all promises and register for each an observer for success and failure (done using currentPromise.add(in:onResolve:onReject:)).

If currentPromise fail we want to abort the entire chain and forward the error by calling reject(err); thanks to the nature of Promise is sufficient to abort the entire chain (even if, at least for now, we don’t support cancel of a running promise).
If currentPromise resolves we decrement the number of remaining promises; if all promises are settled we use a simple map operator to get the result of all promises and resolve allPromise with that array.

.map()

map operator transform an array of objects into Promises and execute them; execution could be parallel or serial; it resolves when all promises resolves, fail if at least one promise fail.

As you can easily guess parallel behaviour is pretty similar to all and, in fact, I’ve implemented it with it:

First of all, as output of the map we return a transformPromise which will be resolved when all input promises are fulfilled.

Using standard Swift’s map function we iterate over all input items and call transform; as output from this closure we expect a Promise (which, at least ideally, it’s based upon pass input).
At the end of the map we got mappedPromise, an array of Promises we can resolve using all operator which also resolve the transformPromise.

Serial version differ a bit: we want to use the then operator to chain returned promise from each transformation and put them as result of a single Promise which return an array:

await()

The last operator which worth to be analysed is await. Using awaityou can write async code in a sync manner:

Due to its closer relationship with GCD queue it’s natural to express awaitas extension of Context struct.

If we want any execution to wait or pause until a block of code finishes its execution and frees up the resources, the most logical way to do it is using GCD’s dispatch semaphores. A dispatch semaphore works same as regular semaphore. Only one exception is, it takes a lesser amount of time to obtain a dispatch semaphore than it takes with traditional system semaphore when resources are available.

A detailed article about this topic can be found here (if you are interested in semaphore theory check out this article).

The flow is:

  • create a new semaphore with one resource available
  • start resolving input promise
  • decrement semaphore via .wait(): with a negative value system block the execution of the queue until signal.
  • once the promise is settled (in another queue) result or error is saved and a signal is sent to the await queue
  • await queue resumes and the value is reported as output (if it’s an error a throw is sent)

What’s next?

Hydra is pretty young project; as open source product I’ll more than happy to accept proposal, new requests and issues reports.
Before moving to 1.0 milestone I’ll complete the await operator by adding async variant you can use to avoid Promise creation.
If you are interested in using it in a commercial product I’ll be also happy to create a section on project’s page.

By daniele

About Me

I'm Daniele Margutti, iOS/macOS Developer, UX Lover and coffe addicted. I love beautiful UI along with clean code; I've also an active open source collaborator, feel free to browse my GitHub repositories.

Currently I'm working at Senior iOS Developer IQUII, a digital agency based in Rome, Italy.

Latest Tweets