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
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)
}
}
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!