Promises
When I started to get into Promises I watch a lot of talks and read a lot of articles. One line stuck with me and sadly I cannot remember who said it but it goes something like: if you want to understand promises you need to implement one yourself. So instead of implementing the frameworks out there I came up with my own solution. Here I want to show in little steps what I have learnt so far. Writing this Article I decided to split it in two or maybe more parts because of its complexity.
Getting to it: a Promise holds an operation that will supply a value once it finished. The simplest way to implement a Promise is a variable to store the operation and a method to start the operation.
// Swift 3
struct Promise<T> {
typealias CompletionCallback = (T) -> Void
private let operation: (CompletionCallback) -> Void
func run(onCompletion: CompletionCallback) {
self.operation {
onCompletion($0)
}
}
}
First of all I use a typealias
to make it more readable. The CompletionCallback
is the callback that gets called when our Promise resolves. The Parameter will be the result of the operation. operation
is the variable which stores the task to perform when the promise gets started. Swift will supply the initializer. Lastly we define the method to run
the Promise. The Parameter of the run
method will be the CompletionCallback
to perform when the promise finishes. The Method is really simple and just calls the operation and inject the resolved value into the CompletionCallback.
A simple usage of a Promise could look like this:
// Swift 3
let p = Promise<Int> { completionCallback in
// Do some async stuff ...
// when async stuff finishes call the callback and handover the computed value
completionCallback(42)
}
p.run { num in
print(num)
}
Here we setup the Promise and define what it should perform once it runs. We need to tell the Promise which type our computed value will be. In this case we choose an Int
. Afterwards we run the task and print the computed value.
The real power of a callback comes when we are able to chain multiple promise together and can handover the resolved value from one promise to the operation of the next. The way to go is to wrap our first Promise into a second one. Which will take the to be computed value T and transform it to another Type U. Let’s try to setup a method that could do just that:
func then<U>() -> Promise<U> {
}
With this signature we can call the then
method on a Promise and it will return a new Promise of type U
.
func then<U>() -> Promise<U> {
return Promise<U> { completionCallback in
}
}
Our return value should be a new Promise of type U
so our method should return one. The then
method will be called from the Promise of type T
so to get the value T
we need to unwrap the promise, or as we called it run
it.
func then<U>() -> Promise<U> {
return Promise<U> { completionCallback in
self.run { result in
}
}
}
Now we have the result of the T-Promise and we somehow have to convert the value of T
to a type U
. We can inject a function as a Parameter for our then
method that can do just that.
func then<U>(_ f: (T) -> U) -> Promise<U> {
return Promise<U> { completionCallback in
self.run { result in
}
}
}
Only thing left is to call the function f
with our result
and tell the completionCallback
that we are done here.
// Swift 3
extension Promise {
func then<U>(_ f: (T) -> U) -> Promise<U> {
return Promise<U> { completionCallback in
self.run { result in
completionCallback(f(result))
}
}
}
}
Let’s see how we can use our new method.
// Swift 3
func intToString(i: Int) -> String {
return "\(i) is a string now"
}
p.then(intToString).run { string in
print(string)
}
We are using our Promise p
from the first example. Notice here that we can reuse our promises and we will get a new computed value from it. A good practice is to not build state inside a Promise, or use state from outside, use dependency injection instead. Back to our example here. I think it is self explanatory. We have a function which will convert a Int
to a String
and our promise p
which will resolve to an Int
gets chained with our intToString
function and eventually resolve to a String
. Another thing to notice is, that the promises only will run when they are triggered. The second promise, let’s called it the string-promise, gets triggerd by the run call. But to complete it needs to run the nested int-promise.
One last case came up when I was playing around with these Promises, what if I already have a function which return a Promise and I want to chain these with my other Promises. Something like this:
// Swift 3
func doSomeMoreAsyncStuff(i: Int) -> Promise<String> {
return Promise { callback in
// async as hell in here
callback("\(i) some number")
}
}
p.then(doSomeMoreAsyncStuff).run { string in
print(string)
}
In this case we need to extend our Promise again and add a new signature
func then<U>(_ f: (T) -> Promise<U>) -> Promise<U> {
return Promise<U> { completionCallback in
self.run { result in
}
}
}
But this time we have two Promise<U>
s one we get when we call f(T)
and one we want to return. Because f
returns an async operation we cannot return it directly in our mehtod. We do need to first unwrap result
then we create our second Promise<U>
by injecting the result
into f
. And since f(result)
returns a Promise we can call run
on it and use our first completionCallback
as its parameter.
// Swift 3
extension Promise {
func then<U>(_ f: (T) -> Promise<U>) -> Promise<U> {
return Promise<U> { completionCallback in
self.run { result in
f(result).run(onCompletion: completionCallback)
}
}
}
}
This Promise here is not complete, and there will be another article to make it more usable but to understand how a Promise work in swift this is a good introduction. Next up I want to tackle error handling and some concrete examples. Coming soon.