Futures and promises under the hood

A good approach to understand how promises work is to look at a possible, but extremely simplified implementation. So, we could start with something like the following to represent our results and errors, and our generic callbacks:

public enum Result<Value> {
case success(Value)
case failure(Error)
}

enum SimpleError: Error {
case errorCause1
case errorCause2
}

typealias Callback<T> = (Result<T>) -> Void
The Result generic defined in the preceding snippet is a particular case of a so-called Either type. Usually, Either types are presented at the same time as Option types, since they are related. Indeed, while Option types can represent a value of a given type or the absence of any value, Either types can represent a value of either of two types. Either types are available in many languages, including Haskell and Scala. Either types are frequently used to represent a value that could either be correct, or be an error. A full-fledged definition for a Result type could be enum Result<V, E>, which is generic both on the type representing correct outcomes and the type representing an error condition. In our case, we have defined Result to be generic on the value only, and will stick to using the Swift Error class for the error condition. It is interesting to know that Result is likely to be part of the Swift 5 standard library.

Now that we have our basic types in place, let's define what we are trying to achieve. Basically, we need two major classes: a Future class representing our read-only, conditionally available value, and a Promise class responsible for resolving it with success or failure. Both classes will be generic to be able to represent any kind of outcome from the asynchronous computations. As a start, we would like to be able to use our Future, as in the following pseudo-code:

// async operation returning a Future
func asyncOperation(...parameters...) -> Future<SomeType>

// get the Future proxying the result of the the async operation
let future : Future<String> = asyncOperation(...arguments...)

// associate a callback to consume the result of our async operation
future.then { result in
// do something with the result
}

A suitable implementation for these requirements is the following:

public class Future<T> {

internal var result : Result<T>? {
didSet {
if let result = result, let callback = callback {
callback(result)
}
}
}
var callback : Callback<T>?

init(_ callback: Callback<T>? = nil) {
self.callback = callback
}

func then(_ callback: @escaping Callback<T>) {
self.callback = callback
if let result = result {
callback(result)
}
}
}

public final class Promise<T> : Future<T> {
func resolve(_ value: T) {
result = .success(value)
}
func reject(_ error: Error) {
result = .failure(error)
}
}
You will notice we opted for having the Promise class derived from Future. This is surely not a clean choice, since it is arguable that a Promise is not a Future: still, it makes our code slightly easier. You will notice, however, that having separate Promise and Future classes is not mandatory, and we could encapsulate all of our functionality in just one class. This is what happens in many libraries that provide an implementation of promises and futures.

As you can see in the preceding snippet, a Future is essentially made of a result object and a callback. When the result is set,  the didSet property observer will call the callback associated with the future, if any. You can assign a callback to the future either at construction time or passing it to the then method.

The Promise class, on the other hand, is really simple, and provides just two methods: resolve and reject, which have responsibility for setting the future result member, thus triggering the execution of the callback.

To use this class, only one last piece of code is required: an asynchronous operation returning a Future. We could define one for testing purposes like in the following snippet. This function does not do anything special: it just waits for a configurable delay, and then it resolves the Future it returns with a fixed string:

import Dispatch

func asyncOperation1(_ delay: Double) -> Promise<String> {
let promise = Promise<String>()
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
DispatchQueue.main.async {
print("asyncOperation1 completed")
promise.result = .success("Test Result")
}
}
return promise
}

We can test our Future implementation by running the following code:

let future : Future<String> = asyncOperation1(1.0)
future.then { result in
switch (result) {
case .success(let value):
print(" Handling result: \(value)")
case .failure(let error):
print(" Handling error: \(error)")
}
}

This will produce the following output:

asyncOperation1 completed
Handling result: Test Result

Great! We have our Future working, but as it stands, it is not entirely useful, since it does not prevent callback hell at all. Indeed, if we wanted to execute a second asynchronous operation using the result of asyncOperation1, we would need to call the second from inside the callback passed to then. What we need is a way to chain successive asynchronous operations, so it becomes possible to write:

let promise2 = asyncOperation2()
promise2.chain { result in
return asyncOperation3(result)
}.chain { result in
return asyncOperation4(result)
}.then { result in
print("THEN: \(result)")
}

We can achieve this by extending our Future class and providing a clever implementation for the chain method. The first thing we should notice is chain will return a Future, so we can actually chain any number of calls to chain. Since we want to be able to chain Futures of different types, the Future that chain returns shall have no resemblance to the Future it is applied to. Furthermore, the two Future classes, the one chain is applied to and the one chain returns, should actually be chained, meaning that, when the former is resolved, the asynchronous operation we want to chain is effectively started, and, when it completes, the latter Future is resolved as well. Here, things may get a little dense, since the asynchronous operation triggered when the first Future is resolved is itself represented by a Future, so we actually have three Future classes involved! With this in mind, it should not be entirely obscure what the following implementation is doing:

extension Future {
func chain<U>(_ cbk: @escaping (T) -> Future<U>) -> Future<U> {
let p = Promise<U>()
self.then { result in
switch result {
case .success(let value): cbk(value).then { r in p.result = r }
case .failure(let error): p.result = .failure(error)
}
}
return p
}
}

Once we have our chain method in place, we can use it to chain multiple asynchronous operations, in the same vein as asyncOperation1:

func asyncOperation2() -> Promise<String> {
let promise = Promise<String>()
DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
DispatchQueue.main.async {
print("asyncOperation2 completed")
promise.resolve("Test Result")
}
}
return promise
}

func asyncOperation3(_ str : String) -> Promise<Int> {
let promise = Promise<Int>()
DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
DispatchQueue.main.async {
print("asyncOperation3 completed")
promise.resolve(1000)
}
}
return promise
}

func asyncOperation4(_ input : Int) -> Promise<Double> {
let promise = Promise<Double>()
DispatchQueue.global().asyncAfter(deadline: .now() + 1.5) {
DispatchQueue.main.async {
print("asyncOperation4 completed")
promise.reject(SimpleError.errorCause1)
}
}
return promise
}

Now, run the following code:

let promise2 = asyncOperation2()
promise2.chain { result in
return asyncOperation3(result)
}.chain { result in
return asyncOperation4(result)
}.then { result in
print("THEN: \(result)")
}

This will give you the following output:

asyncOperation2 completed
asyncOperation3 completed
asyncOperation4 completed
THEN: failure(__lldb_expr_55.SimpleError.errorCause1)

As you can see, in the last example, we have introduced an error condition. That was just for a change! Granted, our implementation of futures is overly simplified, but it remains true to the general idea behind them, and we hope our discussion helped you understand how futures and promises work under the hood. On the other hand, there is really no need here to implement a production-level futures and promises library, since, as we will see shortly, there are already several open source, fully fledged implementations of futures and promises for Swift!

Our discourse about futures and promises and the examples we provided made one thing clear: for futures and promises to be useful at all, we need asynchronous operations that return them. This means that all asynchronous operation from any Cocoa or iOS framework cannot be used out of the box with futures and promises. They need to be wrapped in a function that returns a future that proxies their result. This is another very good reason to resort to an open source implementation of futures and promises, since several of them have gone to great lengths to support as many asynchronous operations from Cocoa and iOS frameworks as possible.