Implement Promise mode in Swift

Keywords: Swift

In asynchronous programming, in addition to competing state processing and resource utilization, another difficulty is process management. In programming languages with anonymous functions and closures, we can usually use callback functions to handle the completion or failure of an asynchronous task. However, when our business logic becomes more and more complex, callback nesting will occur, and the whole event flow will be very chaotic. The model to be discussed today is   Promise. Of course, promise is far from the only way to process asynchronous processes.

Getting Started

I believe you are familiar with the following model in daily development:

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) { 
    // Do something in background.
    dispatch_async(dispatch_get_main_queue()) {
        // Update the UI.
    }
}

Similar usage includes such callbacks as permission authentication and NSURLSession. If the logic is a little more complex, a callback trap will appear. Next, we apply Promise to flatten them, and chain event response can be realized through connection.

Let's see what happens when Promise is used:

doSomething()
    .then( doAnotherThing )
    .then( doSomethingElse )
    .success( someHandler )

Is it a lot clearer in an instant? Let's analyze how to implement it.
First, doSomething() will return a Promise object. The Promise object accepts a function as a parameter in the constructor. This function is used to start asynchronous tasks. This function has two parameters: callback on success and callback on failure.

Let's implement the Promise class below:

class Promise<T> {
    typealias ResolveCallback = (T) -> Void
    typealias RejectCallback = (ErrorType) -> Void
    typealias AsyncTask = (ResolveCallback, RejectCallback) -> Void
    
    let task: AsyncTask
    
    var resolveCallback: ResolveCallback?
    var rejectCallback: RejectCallback?
    
    init(_ task: AsyncTask) {
        self.task = task
    }
}

Generics are used to represent the type of the final result of the asynchronous task. In case of failure, an object implementing the ErrorType protocol is passed as the cause of the error.

Let's look at how to respond to the results and start the task:

    ...

    func success(callback: ResolveCallback) {
        self.resolveCallback = callback
        self.task({ self.resolve($0) }, { self.reject($0) })
    }
    
    func failed(callback: RejectCallback) {
        self.rejectCallback = callback
    }

    ...

I use the cold start method here, that is, the asynchronous task is started only when a response function is mounted. If you want to start the asynchronous task (that is, hot start) as soon as Promise is created, you need to use an attribute to store the task result, so as not to complete the asynchronous task before the response function is mounted, so that the result will be lost.

The method of asynchronous task startup is to pass resolve and reject as parameters to the asynchronous task startup function. When the callback of the asynchronous task itself is called, the Promise object can respond and pass the event to its response function.

Of course, the resolve and reject functions are also very simple, that is, to execute callbacks:

    private func resolve(result: T) {
        self.resolveCallback?(result)
    }
    
    private func reject(error: ErrorType) {
        self.rejectCallback?(error)
    }

call chaining

Up to now, we haven't realized the essence of Promise, and we can't call it chain. The so-called chain call means that when a Promise is completed, a transformation function is used to immediately pass the result to the next Promise for execution, and so on. In this way, we construct a series of operations and start again to realize the cold start I mentioned above.

Then, the core function is then. This function accepts a transformation function as a parameter. This transformation function accepts the Promise result, constructs a new Promise object, and then returns. Since then also returns Promise, we can use chain syntax to continuously connect multiple operations, which is very convenient.

What about the startup sequence after connection? for instance:

promise1.then(genPromise2).then(genPromise3).success(...)

Then the resulting Promise object is like this:

promise3( promise2( promise1 ) )

The Promise object connected by the last then wraps the previous object, and so on. When starting, of course, it is also the last object to be started. However, because this Promise object is encapsulated, it will trigger the previous object first and pass it to itself for execution after getting the result. Still so on, we can ensure that the execution order is correct.

It's very abstract. Just look at the code:

    func then<U>(f: (T) -> Promise<U>) -> Promise<U> {
        return Promise<U> { (resolve, reject) in
            self.task(
                { (result) in
                    let wrapped = f(result)
                    wrapped.success { resolve($0) }
                },
                { (error) in
                    reject(error)
            })
        }
    }

You can see that we have encapsulated another Promise object between the two Promise objects. As the coordinator, it will first execute the previous asynchronous task and then pass it to the next task. Then this function will return the encapsulated Promise object.

Here is the complete code:

class Promise<T> {
    typealias ResolveCallback = (T) -> Void
    typealias RejectCallback = (ErrorType) -> Void
    typealias AsyncTask = (ResolveCallback, RejectCallback) -> Void
    
    let task: AsyncTask
    
    var resolveCallback: ResolveCallback?
    var rejectCallback: RejectCallback?
    
    init(_ task: AsyncTask) {
        self.task = task
    }
    
    private func resolve(result: T) {
        self.resolveCallback?(result)
    }
    
    private func reject(error: ErrorType) {
        self.rejectCallback?(error)
    }
    
    func success(callback: ResolveCallback) {
        self.resolveCallback = callback
        self.task({ self.resolve($0) }, { self.reject($0) })
    }
    
    func failed(callback: RejectCallback) {
        self.rejectCallback = callback
    }
    
    func then<U>(f: (T) -> Promise<U>) -> Promise<U> {
        return Promise<U> { (resolve, reject) in
            self.task(
                { (result) in
                    let wrapped = f(result)
                    wrapped.success { resolve($0) }
                },
                { (error) in
                    reject(error)
            })
        }
    }
}

Use example

Let's try how to use this Promise object. I first define three operations:

func delay(secs: UInt64) -> Promise<Void> {
    return Promise<Void> { (resolve, _) in
        let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC * secs))
        dispatch_after(time, dispatch_get_main_queue()) {
            resolve()
        }
    }
}

func fetch(URL URL: NSURL) -> Promise<NSData?> {
    return Promise<NSData?> { (resolve, reject) in
        let task = NSURLSession.sharedSession().dataTaskWithURL(URL) { (data, response, error) in
            if (error != nil) {
                reject(error!)
            } else {
                resolve(data)
            }
        }
        task.resume()
    }
}

func decodeToString(data: NSData?) -> Promise<String> {
    return Promise<String> { (resolve, _) in
        if (data == nil) {
            resolve("")
        } else {
            resolve(String(data: data!, encoding: NSUTF8StringEncoding) ?? "")
        }
    }
}

These operations include asynchronous operations and synchronous operations, but they can adapt to Promise mode. Each function returns a Promise object.

Then we construct a chain operation:

delay(5)
    .then { () -> Promise<NSData?> in
        fetch(URL: NSURL(string: "https://www.zhihu.com")!)
    }
    .then { (data) -> Promise<String> in
        decodeToString(data)
    }
    .success { (result) in
        print(result)
}

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

Note: my example is done in Playground, so in order for asynchronous tasks to be executed, we need to set a property to prevent Playground from ending the program after the main thread is completed.

The result is in line with our expectation. After a delay of 5 seconds, the network request is executed, transcoded and finally printed.

summary

In fact, the best use of Promise pattern should be JavaScript. We just learn from this practice in other languages and give the implementation of response. Of course, this article only briefly analyzes the internal principle of Promise. Many details may not be perfect. If you like this programming method, you can try a mature one PromiseKit , it has the implementation of Objective-C and Swift.
Of course, you can also try reactive functional programming frameworks with similar ideas, such as ReactiveX.

Posted by raj86 on Tue, 21 Sep 2021 16:21:23 -0700