preface
Recently, I was looking for information on how to use Swift to implement Promise. Because I didn't find a good article, I wanted to write one myself. Through this article, we will implement our own Promise type to understand the logic behind it.
Note that this implementation is completely inappropriate for the production environment. For example, our Promise does not provide any error mechanism, nor does it cover thread related scenarios. I will provide some useful resources and complete implementation links at the end of the article for readers who are willing to dig deeply.
Note: in order to make this tutorial more interesting, I choose to use TDD for introduction. We'll write tests first and then make sure they pass one by one.
First test
Write the first test first:
test(named: "0. Executor function is called immediately") { assert, done in var string: String = "" _ = Promise { string = "foo" } assert(string == "foo") done() }
Through this test, we want to pass a function to Promise's initialization function and call this function immediately.
Note: we don't use any testing framework, just a custom test method that simulates assertions (gist[1]) in Playground.
When we run Playground, the compiler will report an error:
error: Promise.playground:41:9: error: use of unresolved identifier 'Promise' _ = Promise { string = "foo" } ^~~~~~~
Reasonable, we need to define Promise class.
class Promise { }
Run again and the error becomes:
error: Promise.playground:44:17: error: argument passed to call that takes no arguments _ = Promise { string = "foo" } ^~~~~~~~~~~~~~~~~~
We must define an initialization function that accepts a closure as a parameter. And this closure should be called immediately.
class Promise { init(executor: () -> Void) { executor() } }
Thus, we passed the first test. At present, we haven't written anything to boast about, but be patient, and our implementation will continue to grow in the next section.
• Test 0. Executor function is called immediately passed
Let's comment out this test first, because the Promise implementation will be somewhat different in the future.
Minimum
The second test is as follows:
test(named: "1.1 Resolution handler is called when promise is resolved sync") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in resolve(string) } promise.then { (value: String) in assert(string == value) done() } }
This test is simple, but we added some new content to promise class. The promise we created has a resolution handler (i.e. the resolve parameter of the closure), and then it is called immediately (passing a value). Then, we use promise's then method to access value and use assertions to ensure its value.
Before starting the implementation, we need to introduce another different test.
test(named: "1.2 Resolution handler is called when promise is resolved async") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise.then { (value: String) in assert(string == value) done() } }
Unlike test 1.1, the restore method here is called late. This means that in then, value will not be available immediately (because of the 0.1 second delay, resolve has not been called when then is called).
We are beginning to understand the "problem" here. We have to deal with asynchrony.
Our promise is a state machine. Promise is in the pending state when it is created. Once the resolve method is called (with a value), our promise will go to the resolved state and store the value.
The then method can be called at any time regardless of the internal state of promise (that is, whether promise already has a value or not). When the promise is in the pending state, we call then and value will not be available, so we need to store this callback. After that, once promise becomes resolved, we can use resolved value to trigger the same callback.
Now that we have a better understanding of what to implement, let's start by fixing the compiler's error.
error: Promise.playground:54:19: error: cannot specialize non-generic type 'Promise' let promise = Promise<String> { resolve in ^ ~~~~~~~~
We must add generics to promise types. Admittedly, a promise is something like this: it is associated with a predefined type and can retain a value of this type when it is solved.
class Promise<Value> { init(executor: () -> Void) { executor() } }
Now the error is:
error: Promise.playground:54:37: error: contextual closure type '() -> Void' expects 0 arguments, but 1 was used in closure body let promise = Promise<String> { resolve in ^
We must provide a resolve function to pass to the initialization function (i.e. executor).
class Promise<Value> { init(executor: (_ resolve: (Value) -> Void) -> Void) { executor() } }
Note that the resolve parameter is a function that consumes a value: (value) - > void. Once value is determined, this function will be called by the outside world.
The compiler is still unhappy because we need to provide a resolve function to the executor. Let's create a private one.
class Promise<Value> { init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } private func resolve(_ value: Value) -> Void { // To implement // This will be called by the outside world when a value is determined } }
We will implement resolve later when all errors are resolved.
The next error is simple, and the method then has not been defined.
error: Promise.playground:61:5: error: value of type 'Promise<String>' has no member 'then' promise.then { (value: String) in ^~~~~~~ ~~~~
Let's fix it.
class Promise<Value> { init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { // To implement } private func resolve(_ value: Value) -> Void { // To implement } }
Now that the compiler is happy, let's go back to where we started.
As we said before, a Promise is a state machine, which has a pending state and a resolved state. We can use enum to define them.
enum State<T> { case pending case resolved(T) }
The beauty of Swift allows us to directly store promise value in enum.
Now we need to define a state in the Promise implementation, and its default value is. pending. We also need a private function that can update the state when it is still in the. pending state.
class Promise<Value> { enum State<T> { case pending case resolved(T) } private var state: State<Value> = .pending init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { // To implement } private func resolve(_ value: Value) -> Void { // To implement } private func updateState(to newState: State<Value>) { guard case .pending = state else { return } state = newState } }
Note that the updateState(to:) function first checks that it is currently in the. pending state. If promise is already in the. resolved state, it can no longer become another state.
It's time to update the promise state if necessary, that is, when the resolve function is called by the external world passing value.
private func resolve(_ value: Value) -> Void { updateState(to: .resolved(value)) }
It's almost ready, except that the then method has not been implemented. We said that callbacks must be stored and called when promise is resolved. This is to achieve it.
class Promise<Value> { enum State<T> { case pending case resolved(T) } private var state: State<Value> = .pending // we store the callback as an instance variable private var callback: ((Value) -> Void)? init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { // store the callback in all cases callback = onResolved // and trigger it if needed triggerCallbackIfResolved() } private func resolve(_ value: Value) -> Void { updateState(to: .resolved(value)) } private func updateState(to newState: State<Value>) { guard case .pending = state else { return } state = newState triggerCallbackIfResolved() } private func triggerCallbackIfResolved() { // the callback can be triggered only if we have a value, // meaning the promise is resolved guard case let .resolved(value) = state else { return } callback?(value) callback = nil } }
We define an instance variable callback to retain the callback when promise is in the. pending state. At the same time, we create a method triggerCallbackIfResolved, which first checks whether the status is. Resolved, and then passes the unpacked value to the callback. This method is called in two places. One is in the then method, if promise has been resolved when calling then. The other is in the updateState method, because that is where promise updates its internal state from. pending to. Resolved.
With these modifications, our test passed successfully.
• Test 1.1 Resolution handler is called when promise is resolved sync passed (1 assertions) • Test 1.2 Resolution handler is called when promise is resolved async passed (1 assertions)
We're on the right track, but we still need to make a little change to get a real Promise implementation. Let's take a look at the test first.
test(named: "2.1 Promise supports many resolution handlers sync") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in resolve(string) } promise.then { value in assert(string == value) } promise.then { value in assert(string == value) done() } }
test(named: "2.2 Promise supports many resolution handlers async") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise.then { value in assert(string == value) } promise.then { value in assert(string == value) done() } }
This time we called then twice for each promise.
First look at the test output.
• Test 2.1 Promise supports many resolution handlers sync passed (2 assertions) • Test 2.2 Promise supports many resolution handlers async passed (1 assertions)
Although the test passed, you may also pay attention to the problem. Test 2.2 has only one assertion, but it should be two.
If we think about it, it's actually logical. Admittedly, in asynchronous test 2.2, promise was still in the. pending state when the first then was called. As we saw earlier, we stored the callback of then for the first time. However, when we call then the second time, promise is still in the. pending state, so we replace the callback erase with a new one. Only the second callback will be executed in the future, and the first will be forgotten. This makes the test pass, but only one assertion instead of two.
The solution is also simple: store an array of callbacks and trigger them when promise is resolved.
Let's update it.
class Promise<Value> { enum State<T> { case pending case resolved(T) } private var state: State<Value> = .pending // We now store an array instead of a single function private var callbacks: [(Value) -> Void] = [] init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { executor(resolve) } func then(onResolved: @escaping (Value) -> Void) { callbacks.append(onResolved) triggerCallbacksIfResolved() } private func resolve(_ value: Value) -> Void { updateState(to: .resolved(value)) } private func updateState(to newState: State<Value>) { guard case .pending = state else { return } state = newState triggerCallbacksIfResolved() } private func triggerCallbacksIfResolved() { guard case let .resolved(value) = state else { return } // We trigger all the callbacks callbacks.forEach { callback in callback(value) } callbacks.removeAll() } }
The test passed with two assertions.
• Test 2.1 Promise supports many resolution handlers sync passed (2 assertions) • Test 2.2 Promise supports many resolution handlers async passed (2 assertions)
congratulations! We have created our own Promise class. You can already use it to abstract asynchronous logic, but it has limitations.
Note: from a global perspective, we know that then can be renamed observe. Its purpose is to consume the value after promise is solved, but it does not return anything. This means that we can't connect multiple promises for the time being.
Concatenate multiple promises
If we can't concatenate multiple promises, our Promise implementation is not complete.
Let's take a look at the test, which will help us implement this feature.
test(named: "3. Resolution handlers can be chained") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise .then { value in return Promise<String> { resolve in after(0.1) { resolve(value + value) } } } .then { value in // the "observe" previously defined assert(string + string == value) done() } }
As the test shows, the first then creates a new Promise with a new value and returns it. The second then (which we defined in the previous section, called observe) is concatenated to access the new value (which will be "foofoo").
We soon saw an error in the terminal.
error: Promise.playground:143:10: error: value of tuple type '()' has no member 'then' .then { value in ^
We must create a then overload that accepts a function that returns promise. In order to call then in series, this method must also return a promise. The prototype of then is as follows.
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { // to implement }
Note: careful readers may have found that we are implementing flatMap for Promise. Just like defining flatMap for Optional and Array, we can also define it for Promise.
Here comes the difficulty. Let's take a step-by-step look at how to implement then of this "flatMap".
- We need to return a promise < newvalue >
- Who gave us such a promise? onResolved method
- But onResolved requires a parameter of type value. How do we get this value? We can use the previously defined then (or "observe") to access it when it is available
If it is written as code, it is roughly as follows:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { then { value in // the "observe" one let promise = onResolved(value) // `promise` is a Promise<NewValue> // problem: how do we return `promise` to the outside ?? } return // ??!! }
It's almost ready. But we still have a small problem to fix: this promise variable is limited by the closure passed to then. We cannot use it as the return value of a function.
The trick we need to use is to create a wrapper promise < newvalue >, which will execute the code we have written so far, and then be solved at the same time when the promise variable is solved. In other words, when the promise provided by the onResolved method is solved and a value is obtained from the outside, the wrapped promise is solved and the same value is obtained.
The text may be a little abstract, but if we write code, we will see it more clearly:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { // We have to return a promise, so let's return a new one return Promise<NewValue> { resolve in // this is called immediately as seen in test 0. then { value in // the "observe" one let promise = onResolved(value) // `promise` is a Promise<NewValue> // `promise` has the same type of the Promise wrapper // we can make the wrapper resolves when the `promise` resolves // and gets a value promise.then { value in resolve(value) } } } }
If we tidy up the code, we will have two methods:
// observe func then(onResolved: @escaping (Value) -> Void) { callbacks.append(onResolved) triggerCallbacksIfResolved() } // flatMap func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { return Promise<NewValue> { resolve in then { value in onResolved(value).then(onResolved: resolve) } } }
Finally, the test passed.
• Test 3. Resolution handlers can be chained passed (1 assertions)
Concatenate multiple value s
If you can implement flatMap for a type, you can use flatMap to implement Map for it. What should a map look like for our Promise?
We will use the following tests:
test(named: "4. Chaining works with non promise return values") { assert, done in let string: String = "foo" let promise = Promise<String> { resolve in after(0.1) { resolve(string) } } promise .then { value -> String in return value + value } .then { value in // the "observe" then assert(string + string == value) done() } }
Note that the first then does not return a Promise, but transforms the value it receives. The new then corresponds to the map we want to add.
The compiler reports an error saying that we must implement this method.
error: Promise.playground:174:26: error: declared closure result 'String' is incompatible with contextual type 'Void' .then { value -> String in ^~~~~~ Void
This method is very close to flatMap. The only difference is that its parameter onResolved function returns a NewValue instead of promise < NewValue >.
// map func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { // to implement }
We said before that we can use flatMap to implement map. In our case, we see that we need to return a promise < newvalue >. If we use then of the "flatMap" and create a promise, and then solve it directly with the mapped value, we'll get it done. Let me prove it.
// map func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { return then { value in // the "flatMap" defined before // must return a Promise<NewValue> here // this promise directly resolves with the mapped value return Promise<NewValue> { resolve in let newValue = onResolved(value) resolve(newValue) } } }
Again, the test passed.
• Test 4. Chaining works with non promise return values passed (1 assertions)
If we remove the comments, let's see what we've done. We have three implementations of the then method that can be used or concatenated.
// observe func then(onResolved: @escaping (Value) -> Void) { callbacks.append(onResolved) triggerCallbacksIfResolved() } // flatMap func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { return Promise<NewValue> { resolve in then { value in onResolved(value).then(onResolved: resolve) } } } // map func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { return then { value in return Promise<NewValue> { resolve in resolve(onResolved(value)) } } }
Use example
The realization comes to an end. Our Promise class is complete enough to show what we can do with it.
Suppose our app has some users, and the structure is as follows:
struct User { let id: Int let name: String }
Suppose we still have two methods, one is to get the list of user IDs, and the other is to use id to get a user. And suppose we want to display the name of the first user.
Through our implementation, we can do this by using the three then defined earlier.
func fetchIds() -> Promise<[Int]> { ... } func fetchUser(id: Int) -> Promise<User> { ... } fetchIds() .then { ids in // flatMap return fetchUser(id: ids[0]) } .then { user in // map return user.name } .then { name in // observe print(name) }
The code becomes very readable, concise, and not nested.
conclusion
At the end of this article, I hope you like it.
You can find the complete code in this gist. If you want to further understand, here are some resources I use.
- Promises in Swift by Khanlou[2]
- JavaScript Promises ... In Wicked Detail[3]
- PromiseKit 6 Release Details[4]
- TDD Implementation of Promises in JavaScript[5]
From: Implementing Promises in Swift[6]
reference material
[1] gist: https://gist.github.com/felginep/039ca3b21e4f0cabb1c06126d9164680
[2] Promises in Swift by Khanlou: http://khanlou.com/2016/08/promises-in-swift/
[3] JavaScript Promises ... In Wicked Detail: https://www.mattgreer.org/articles/promises-in-wicked-detail/
[4] PromiseKit 6 Release Details: https://promisekit.org/news/2018/02/PromiseKit-6.0-Released/
[5] TDD Implementation of Promises in JavaScript: https://www.youtube.com/watch?v=C3kUMPtt4hY
[6] Implementing Promises in Swift: https://felginep.github.io/2019-01-06/implementing-promises-in-swift