on
Implementing Promises in Swift
I recently have been looking for some resources on how to implement a promise in Swift, and because I did not find any good articles about it, I thought I could write one instead. In this article we’ll implement our own Promise
type in order to understand the logic behind it.
Note that the implementation is far from production ready and should not be used as is. For instance, our promise will not provide any error mechanism and threading will not be covered. I’ll link useful resources and complete implementations at the end of this article for those who want to dig deeper.
Note: To make this tutorial a little more interesting, I chose to do it in TDD. We will write tests first and make them pass one by one.
Our first test
Let’s write our first test.
In this test we want to implement that the function passed to the initializer of the promise is called immediately.
Note: For those who may wonder, we do not use any test framework here, but a custom method test
that simulates assertions in the playground (gist).
When we run the playground, the compiler raises a first error:
Fair enough, we need to define the Promise
class.
The error now becomes:
We have to define an initiliazer that takes a closure as an argument. And this closure should be called immediately.
This makes our first test pass! We have almost nothing for now, but be patient, our implementation will grow in the next section!
We can comment this test out as the implementation of Promise
will change a bit in the future.
The bare minimum
Our second test is the following:
The test is quite simple, but we add some content to the Promise
class. We create a promise with a resolution handler (the resolve
parameter of the closure) and call it right away with a value.
In a second time, we use the then
method on the promise to access the value and make an assertion about it.
Before diving into the implementation, we have to introduce a slightly different test at the same time.
Contrary to the test 1.1
, the resolve
method is called after a delay. That means the value will not be available in the then
right away (because the 0.1 seconds are not passed yet when we call then
).
We can start understanding the “problem” here. We have to deal with asynchronicity.
Our promise is a state machine. When it is first created, the promise is in a pending
state. Once the resolve
method is called with a value, our promise pass in a resolved
state and store this value.
The method then
can be called anytime, whatever the internal state of the promise (meaning before or after the promise has a value). If the promise is in pending
state and we call then
on it, the value is not available, so we have to store the callback parameter. And once the promise becomes resolved
, we can trigger this same callback with the resolved value.
Now that we understand a little better what we have to implement, let’s start by fixing the compiler issues.
We have to make our Promise
type generic. Indeed, a promise is associated with a predefined type and will hold a value of this type once resolved.
Now the error becomes:
We have to provide a resolve
function to the closure passed in the initializer (the executor).
Note here that the resolve parameter is a function that consume a value: (Value) -> Void
. This function will be called by the outside world once the value is determined.
The compiler is still not happy because we need to provide a resolve
function to the executor
. Let’s create one that will be private
.
We will implement resolve
in a moment, when all the errors will be taken care of.
The next one is simple, the method then
is not defined yet.
Let’s fix that.
Now that the compiler is happy, let’s go back where we were before.
We previously said that a Promise
is a state machine with a pending
and a resolved
state. We can define these states with an enum:
The beauty of Swift makes it possible to store the value of the promise directly in the enum.
Now we have to define a default state of .pending
in our Promise
implementation. And we also need a private function that can update the state in case the promise is still in a .pending
state.
Note that the updateState(to:)
function first checks if the promise is in the .pending
state. If the promise is already in the .resolved
state, it can’t move to another state and will stay .resolved
forever.
Now it’s time to update the state of the promise when needed, meaning when the resolve
function is called from the outside world with a value.
We are almost done, but there is still the then
method to implement. We said we had to store the callback parameter and call this callback when the promise resolves. Let’s implement that.
We define an instance variable callback
that holds the callback while the promise is .pending
. We also create a method triggerCallbackIfResolved
that first checks if the state is .resolved
, unwraps the associated value, and pass it to the callback. This method is called at two locations. In the then
method if the promise is already resolved at the time we call then
. And in the updateState
method, because that’s where the promise updates its state from .pending
to .resolved
.
With these modifications, our tests pass successfully.
We are on the right path, but there is still a slight change we have to make to get a first real Promise
implementation. Let’s look at the tests first.
This time we call then
twice on the promise.
Let’s look at the tests outputs.
The tests pass, but you may have spotted the issue. The test 2.2
has only one assertion, but should have two.
If we think about it, it’s logical. Indeed, in the async version (2.2
), when the first then
is called the promise is still .pending
. As we have seen earlier, we store the callback of the first then
. But when we call the then
for the second time, the promise hasn’t resolved yet and is still .pending
, so we erase the callback with the new one. Only the second callback will be executed, the first one being forgotten. That makes the test pass, but with only one assertion instead of two.
The solution here is to store an array of callbacks and to trigger all the callbacks when the promise resolves.
Let’s update our solution with this small update.
The tests now pass with both two assertions.
Congratulations! We have created the base of our Promise
class. You can already use it to abstract asynchronicity but it’s still limited.
Note: if we take a look at the global picture here, we can see that the then
we have defined could be renamed observe
. Its purpose is to consume the value of the promise once resolved, but it does not return anything. Meaning we can’t chain promises for now.
In the next sections we will create overloads of then
in order to return new promises or new values along the way.
Chaining Promises
Our Promise
implementation would not be complete if we can’t chain multiple promises.
Let’s look at the test that will help us implement this feature.
We can see here that the first then
creates a new Promise
with a whole new value and returns it. The second then
(the one we defined in the previous section, that we called observe
) is chained to access the new value (that will hold "foofoo"
here).
This immediately raises an error in the console.
We have to create an overload of then
that takes a function that returns a promise. And in order to chain other calls of then
, the method has to return a promise too. The prototype of this new method then
is the following.
Note: Attentive readers may have spotted that we are implementing flatMap
on the Promise
type here. In the same way flatMap
is defined for Optional
or Array
, we can define it for the Promise
type.
The “difficulty” starts here. Let’s walk through the implementation of this “flatMap” then
step by step.
- We have to return a
Promise<NewValue>
. - What gives us such a promise? The
onResolved
method does. - But
onResolved
takes a value of typeValue
in parameter. How can we get this value? We can use the previously definedthen
(or “observe
”) to access it when available.
If we write this down, here what we get for now:
We are almost there. There is still a small problem to fix: the promise
variable is captured in the closure passed to then
. We can’t use it as a return value for the function.
The trick here is to create a wrapping Promise<NewValue>
that will execute what we wrote so far, and that will resolve at the same time the promise
variable is resolved. In other words, when the promise provided by the onResolved
method will resolve and get a value from the outside, the wrapping promise will resolve too and get the same value.
That may be a little abstract, but if we write it, we will see it better:
If we clean the code a bit we now have these two methods :
And finally, the test pass.
Chaining values
If you can implement flatMap
on a type, you can implement map
on this same type using flatMap
. What does map
looks like for our Promise
?
The test we will use here is the following:
Note here that the first then
that is used does not return a Promise
anymore, but transforms the value it receives. This is a new then
and corresponds to the map
version we want to add.
The compiler emits an error saying we have to implement this method.
The prototype is very close to the flatMap
version, the only difference is that we return a NewValue
instead of a Promise<NewValue>
in the onResolved
function.
We said earlier that we can use flatMap
to implement map
. In our case we see we need to return a Promise<NewValue>
. If we use the “flatMap” then
of the previous section and create a promise that directly resolved with the mapped value, we have finished. Let’s prove it.
Once again the test pass.
If we remove the comments and look at what we achieved, we have three then
methods implemented that can be used and chained.
Example of use
We will stop here for the implementation. Our Promise
class is complete enough to demonstrate what we can do with it.
Let’s imagine we have some users in our app, that we store in the following struct:
Let’s also say we have two methods at our disposable, one that fetch a list of user ids, and one that fetch a user from its id. And let’s say we want to display the name of the first user.
Here is how we can do it very simply with our implementation, using the three then
we previously defined.
The code becomes highly readable, flat and concise!
Conclusion
That’s the end of this article, I hope you liked it.
You can find the whole code in this gist. And if you want to dig deeper, here are the sources I used.