Synchronous and Asynchronous

Generally we divide message communication into synchronous and asynchronous, where synchronous means that the sender of the message has to wait for the message to return before proceeding with other things, and asynchronous means that the sender of the message does not have to wait for the message to return before proceeding with other things. Asynchrony allows us to do more things at once, while achieving higher performance. Asynchrony is also flexible and complex, which is not the focus of this article.

Code blocks and closures

In the GCD (Grand Central Dispatcher), if we want to perform some operations in the non-main thread, then we need the help of closures.

func asyncFunction(callback:(Result) -> Void) - >Void {
	dispatch_async(queue) {
		let x = Result("Hello!")
		callback(x)
	}
}
Copy the code

Encapsulates an asyncFunction method from which I create some kind of Result, but if I want to call back the Result, THEN I have to provide a callback. I’m sure you’ve written code like this.

Some problems with callback closures

The callback hell

When a task needs to be completed in multiple asynchronous phases, code for the next phase’s callback is added to each phase’s callback function, resulting in pyramid-like code like this:

func processImageData1(completionBlock: (result: Image) -> Void) { 
    loadWebResource("dataprofile.txt") { dataResource in 
        loadWebResource("imagedata.dat") { imageResource in 
            decodeImage(dataResource, imageResource) { imageTmp in 
                dewarpAndCleanupImage(imageTmp) { imageResult in 
                    completionBlock(imageResult)
                 }
             }
         }
     }
}

processImageData1 { image in display(image)} 
Copy the code

This nested callback makes problem tracking difficult. You can imagine how scary the code can be as the callback hierarchy continues to grow. Futures/Promises will get us out of a pullback.

The Future and Promise

Future&Promise has its roots in functional languages, the concept of which has been around since 1977. The goal is to simplify the handling of asynchronous code by separating a value from the method that generates it.

A Future is a read-only container of values that will be computed at some point in the Future (the behavior that generates this value is an asynchronous operation). A Promise is a writable container for setting the value of a Future.

A Promise is something you make to someone else.

In the Future you may choose to honor (resolve) that promise, or reject it.

Here’s an example of how powerful they can be:

Use the conventional callback writing method


class UserLoader {
    typealias Handler = (Result<User- > >)Void
    
    func loadUser(withID id: Int, completionHandler: @escaping Handler) {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        let task = urlSession.dataTask(with: url) { [weak self] data, _, error in
            if let error = error {
                completionHandler(.error(error))
            } else {
                do {
                    let user: User = try unbox(data: data ?? Data())

                    self? .database.save(user) { completionHandler(.value(user)) } }catch {
                    completionHandler(.error(error))
                }
            }
        }

        task.resume()
    }
}
Copy the code

usingFutures & PromisesAfter that, the code will change to

class UserLoader {
    func loadUser(withID id: Int) -> Future<User> {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        return urlSession.request(url: url)
                         .unboxed()
                         .saved(in: database)
    }
}

// Call the code
let userLoader = UserLoader()
userLoader
    .loadUser(withID: userID)
    .observe { result in
    / / processing result
}
Copy the code

It’s easy to see how with Futures & Promises, the amount of code is instantly reduced by more than half. What’s the catch?

Future

A Future, as mentioned above, is a container that returns a read-only value after an asynchronous operation. Assigning a value to a Future will trigger calls to callback methods in the callback list.

enum Result<T> {
    case value(T)
    case error(Error)}class Future<Value> {
    fileprivate var result: Result<Value>? {
        // Observe whenever a result is assigned, and report it
        didSet { result.map(report) }
    }
    private lazy var callbacks = [(Result<Value- > >)Void] ()func observe(with callback: @escaping (Result<Value>) -> Void) {
        callbacks.append(callback)

        // If a result has already been set, call the callback directly
        result.map(callback)
    }

    private func report(result: Result<Value>) {
        for callback in callbacks {
            callback(result)
        }
    }
}
Copy the code

Promise

Promise is a Future subclass that provides Promise methods for honoring and rejecting. Fulfilling a promise sets the Future to a completed value. Rejecting a promise sets an error to the Future.

class Promise<Value> :Future<Value> {
    init(value: Value? = nil) {
        super.init(a)// If the value was already known at the time the promise
        // was constructed, we can report the value directly
        result = value.map(Result.value)
    }

    func resolve(with value: Value) {
        result = .value(value)
    }

    func reject(with error: Error) {
        result = .error(error)
    }
}
Copy the code

We can add an Extension to our URLSession based on the above implementation:

extension URLSession {
    func request(url: URL) -> Future<Data> {
        // Start by constructing a Promise, that will later be
        // returned as a Future
        let promise = Promise<Data> ()// Perform a data task, just like normal
        let task = dataTask(with: url) { data, _, error in
            // Reject or resolve the promise, depending on the result
            if let error = error {
                promise.reject(with: error)
            } else {
                promise.resolve(with: data ?? Data())
            }
        }
        task.resume()
        return promise
    }
}
Copy the code

With this in place, we can invoke a simple network request

URLSession.shared.request(url: url).observe { result in
    // Handle result
}
Copy the code

Chaining

return urlSession.request(url: url)
                 .unboxed()
                 .saved(in: database)

Copy the code

The network request is encapsulated, but how is unboxed and saved implemented?

Before implementing these methods, we need to implement a fundamental method called Chained:

extension Future {
    func chained<NextValue>(with closure: @escaping (Value) throws -> Future<NextValue- > >)Future<NextValue> {
        // Start by constructing a "wrapper" promise that will be
        // returned from this method
        let promise = Promise<NextValue> ()// Observe the current future
        observe { result in
            switch result {
            case .value(let value):
                do {
                    // Attempt to construct a new future given
                    // the value from the first one
                    let future = try closure(value)

                    // Observe the "nested" future, and once it
                    // completes, resolve/reject the "wrapper" future
                    future.observe { result in
                        switch result {
                        case .value(let value):
                            promise.resolve(with: value)
                        case .error(let error):
                            promise.reject(with: error)
                        }
                    }
                } catch {
                    promise.reject(with: error)
                }
            case .error(let error):
                promise.reject(with: error)
            }
        }

        return promise
    }
}
Copy the code

In Chained we can easily implement saved:

extension Future where Value: Savable {
    func saved(in database: Database) -> Future<Value> {
        return chained { user in
            let promise = Promise<Value>()

            database.save(user) {
                promise.resolve(with: user)
            }

            return promise
        }
    }
}
Copy the code

Transforms

Although chaining provides a way to perform asynchronous operations sequentially, sometimes you just want to do a simple synchronous transformation to get the values inside. To do this we’ll add an transformed method to the Future:

extension Future {
    func transformed<NextValue>(with closure: @escaping (Value) throws -> NextValue) - >Future<NextValue> {
        return chained { value in
            return try Promise(value: closure(value))
        }
    }
}
Copy the code

The conversion is a synchronous version of the link operation, and the value is converted when a new Promise is constructed. This kind of operation has a good fit for JSON parsing or converting a value from one type to another.

We can convert a Future whose value is type Data to a Future whose value is type Unboxable:

extension Future where Value= =Data {
    func unboxed<NextValue: Unboxable>(a) -> Future<NextValue> {
        return transformed { try unbox(data: $0)}}}Copy the code

integration

With the Future & Promises implementation above, UserLoader can be implemented happily

class UserLoader {
    func loadUser(withID id: Int) -> Future<User> {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        // Request the URL, returning data
        let requestFuture = urlSession.request(url: url)

        // Transform the loaded data into a user
        let unboxedFuture: Future<User> = requestFuture.unboxed()

        // Save the user in the database
        let savedFuture = unboxedFuture.saved(in: database)

        // Return the last future, as it marks the end of the chain
        return savedFuture
    }
}
Copy the code

The above way of calling is not found to feel the same as in calling synchronous code.

With chained calls, the code will be handled the way we mentioned at the beginning:

class UserLoader {
    func loadUser(withID id: Int) -> Future<User> {
        let url = apiConfiguration.urlForLoadingUser(withID: id)

        return urlSession.request(url: url)
                         .unboxed()
                         .saved(in: database)
    }
}
Copy the code

Above is a simplified version of Future&Promise. There are many open source libraries available on Github:

  • BrightFutures
  • FutureKit
  • PromiseKit – The most popular one
  • SwiftNIO – not an actual promise library, but it has a beautifully written event loop based promise implementation under the hood

SwiftNIO

Why this article mentions the concept of Future/Promise? SwiftNIO. This concept has also been incorporated into SwiftNIO. At Vapor3 we would see a lot of returns like Future, and then there would be a lot of chained method calls.

  • flatMap
  • map
  • transform
  • flatten
  • do/catch
  • catchMap/catchFlatMap
  • always
  • wait
  • request.future(_:)

If you haven’t been exposed to this concept at first, it may be difficult to understand.

So how does SwiftNIO implement this asynchronous mechanism?

The source code in this

We’ll talk about that next time!

conclusion

We call back from an initial UserLoader closure to a chained call to Future&Promise to let us know that asynchrony can be handled in this mode. The core point Future represents a Future value. A Promise is used to set the value of the Future. Chaining (an operation on an existing Future to obtain the value of the current Future is handled by the Promise. Return a new Future. It’s a powerful weapon, but take a look.

Refer to the reading

  • Under the hood of Futures & Promises in Swift
  • Swift Concurrency Manifesto

To read more, you can subscribe to the official wechat official account: