- Original address: Under the hood of Futures & Promises in Swift
- Original John Sundell
- Translator: Ooatuo
- Reviewer: kangkang, Richard_lee
Explore the Futures & Promises in Swift
Asynchronous programming can be said to be one of the most difficult parts of building most applications. Whether it's dealing with background tasks, such as network requests, performing heavy operations in parallel in multiple threads, or delaying execution of code, these tasks tend to break and make it difficult to debug problems.
Because of this, many solutions have been invented to solve these problems-primarily creating abstractions around asynchronous programming, making them easier to understand and infer. For most solutions, they are all helpful in "callback hell", that is, when you have multiple nested closures in order to handle different parts of the same asynchronous operation.
This week, let's look at a solution like this- Futures & Promises -let's open the hood and see how they work.
A Promise about the future
When introducing the concept of Futures & Promises, most people first ask what is the difference between the future and the Promise? . In my opinion, the simplest and most understandable understanding is this:
- Promise is the promise you made to others.
- In the future, you may choose to honor (resolve) this promise, or reject it.
If we use the definition above, Futures & Promises becomes the positive and negative of a coin. A Promise is constructed and then returned to a future where it can be used to extract information later.
So what does this look like in the code?
Let's look at an asynchronous operation where we load a "User" data from the network, convert it to a model, and finally save it to a local database. With an "old-fashioned approach", closures, it looks like this:
Class Userloader {Typealias Handler = (result<user>), Void func loaduser (Withid id:int, Completionhandl ER: @escaping Handler) { Leturl = apiconfiguration.urlforloadinguser (withid:id) LetTask = Urlsession.datatask (with:url) {[Weak self] data, _, errorinch if LetError = Error {Completionhandler (. Error)}Else{ Do{ LetUser:user = Try unbox (data:data?? Data ()) self?. Database.save (user) {Completionhandler (. value (User)}}} ' catch { Completionhandler (. Error (Error))}}} task.resume ()}
As we can see, even with a very simple (very common) operation, we eventually get quite deep nesting code. This is replaced with the Future & Promise:
class UserLoader { func loadUser(withID id: Int) -> Future<User> { let url = apiConfiguration.urlForLoadingUser(withID: id) return urlSession.request(url: url) .unboxed() .saved(in: database) }}
This is the method of invocation:
letin // Handle result}
Now the code above may seem a bit dark magic (where are all the other codes?!?? , so let's take a closer look at how it's implemented.
Explore the future
Like most things in programming, there are many different ways to implement Futures & Promises. In this article, I'll provide a simple implementation, and finally there will be links to popular frameworks that provide more functionality.
Let's start by exploring the Future
implementation, which is publicly returned from the asynchronous operation. It provides a read-only way to observe whenever a value is assigned and to maintain a list of observed callbacks, like this:
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) set, call the callback directly result.map(callback) } private func report(result: Result<Value>) { forin callbacks { callback(result) } }}
Generate Promise
Next, on the opposite side of the coin, Promise
is Future
the subclass that is used to add the solution * and rejects * its API. The result of resolving a promise is to successfully complete and return a value in the future, and rejecting it will result in an error. Like this:
class Promise<Value>: Future<Value> { init(value: Value? = nil) { super.init() // 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) }}
As you can see, the basic implementation of Futures & Promises is very simple. Much of the magic we get from using these methods is that these extensions can increase the way we chain and change the future, enabling us to build these beautiful chain of operations, as we did in Userloader.
However, if you do not add an API for chained operations, we can construct a user to load the first part of the asynchronous chain- urlsession.request (URL:)
. One common practice in asynchronous abstractions is to provide convenient APIs on top of the SDK and the Swift standard library, so we'll do that here too. The request (URL:)
method will be an extension of urlsession
, allowing it to be used as an future/promise-based API.
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 letin // Reject or resolve the promise, depending on the result iflet error = error { promise.reject(with: error) else { promise.resolve(with: data ?? Data()) } } task.resume() return promise }}
We can now perform network requests by simply doing the following:
in // Handle result}
Chain type
Next, let's look at how multiple future combinations can be combined to form a chain-for example, when we load the data, unpack it and save the instance to the database in Userloader.
A chained notation involves providing a closure that can return a new value to the future. This will enable us to get the result from an operation, pass it to the next operation, and return a new value from the operation. Let's take a look:
Extension Future {func chained<nextvalue> (with closure: @escaping (Value) throws-future<nextvalue>) -future<nextvalue> {//Start by constructing a"wrapper"Promise that'll be//returned from the This method LetPromise = Promise<nextvalue> ()//Observe The current future Observe {resultinchSwitch result { Case. Value ( LetValue): Do{//attempt to construct a new future given//the value from the first one LetFuture = Try Closure (value)//Observe the"Nested"Future, and once it//completes, resolve/reject the"wrapper"Future Future.observe {ResultinchSwitch result { Case. Value ( LetValue): Promise.resolve (With:value) Case. Error ( LetError): Promise.reject (With:error)}} } catch {Promise.reject (With:error)} Case. Error ( LetError): Promise.reject (With:error)}}returnPromise}}
Using the method above, we can now add an extension to the future of Savable
the type to ensure that the data can be easily saved to the database once it is available.
where Value: Savable { func saved(in database: Database) -> Future<Value> { returnin let promise = Promise<Value>() database.save(user) { promise.resolve(with: user) } return promise } }}
Now that we're digging into the real potential of Futures & Promises, we can see how easy it is to extend the API, because we can Future
use different common constraints in our classes to easily add convenient APIs for different values and operations.
Transformation
While chained calls provide a powerful way to perform asynchronous operations in an orderly manner, sometimes you just want to do a simple synchronous conversion of values-for this we add support for transformations .
Transformations are done directly and can be arbitrarily thrown, which is perfect for JSON parsing or converting one type of value to another type. Just like that chained()
, we'll add a transformed()
method as an Future
extension, like this:
extension Future { func transformed<NextValue>(with closure: @escaping (Value) throws -> NextValue) -> Future<NextValue> { returnin return try Promise(value: closure(value)) } }}
As you can see above, the transformation is actually a synchronous version of a chained operation because its value is directly known-it is built just to pass it to a new one Promise
.
Using our new transformation API, we can now add support to transform the future of Data
a type into a future Unboxable
type of type (JSON-decoded), like this:
where Value == Data { func unboxed<NextValue: Unboxable>() -> Future<NextValue> { return$0) } }}
Integrate all
Now we have all the parts that have been UserLoader
upgraded to support Futures & Promises. I'll break the operation down into each row, so it's easier to see what happens at each step:
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() in the database let savedFuture = unboxedFuture.saved(in: database) // Return the last future, as it marks the end of the chain return savedFuture }}
Of course, we can also do what we've just started, and string together all the calls (which gives us the benefit of using Swift's type inference to infer the future User
of the type):
class UserLoader { func loadUser(withID id: Int) -> Future<User> { let url = apiConfiguration.urlForLoadingUser(withID: id) return urlSession.request(url: url) .unboxed() .saved(in: database) }}
Conclusion
Futures & Promises is a very powerful tool when writing asynchronous code, especially when you need to combine multiple operations and transformations. It almost makes it possible for you to write asynchronous code like synchronization, which improves readability and makes it easier to move when you need it.
However, as with most abstractions, you are essentially masking complexity and moving most of the heavy lifting to the background. So, while the urlSession.request(url:)
API looks good from the outside, it becomes more difficult to debug and understand what is going on.
My advice is that if you are using Futures & Promises, that is to make your call chain as concise as possible. Keep in mind that good documentation and reliable unit testing can help you avoid a lot of hassle and tricky debugging.
Here are some popular Swift versions of the Futures & Promises Open source framework:
- Promisekit
- Brightfutures
- When
- Then
You can also find all the code involved in this article on GitHub.
If you have any questions, please leave a message. I very much hope to hear your suggestion!?? You can leave a message below, or contact me @johnsundell Twitter.
In addition, you can get the latest Sundell Swift podcasts, and I and the Community visitors will answer your questions about swift development.
Thanks for reading??。
Upload ImageCtrl or? + EnterReviews
Explore Futures & Promises in Swift