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 & Promises
After 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: