A lightweight Promise
implementation.
A common pattern used by developers for handling asynchronous results is to inject a completion handler closure as a function argument to handle the result. For instance:
func fetchUser(with id: User.ID, completion: @escaping (Result<User, Error>) -> Void) {
apiRequest {
completion($0)
}
}
// call site:
fetchUser(with: 10) { result in
switch result {
case .success(let user):
...
case .failure(let error):
...
}
}
Using Promise
pattern we can instead solve the problem like this:
func fetchUser(with id: User.ID) -> Promise<User> {
Promise { seal in
apiRequest {
seal.resolve(with: $0)
// or seal.reject(with: Error) if error occurred
}
}
}
// call site:
let promise = fetchUser(with: 10)
...
promise.then { value in
...
}
promise.catch { error in
...
}
// or
promise.finally {
switch $0 {
case .success(let user):
...
case .failure(let error):
...
}
}
The advantage of using Promises is that they simplify asynchronous programming so that we as programmers can focus on what instead of how. This especially becomes apparent when dealing with several asynchronous operations at the same time.
Often, we find ourselves writing code that looks something like this:
func download(_ handler: @escaping (Result<C, Error>) -> Void) {
downloadA { resultA in
switch resultA {
case .success(let a):
downloadB(input: a) { resultB in
switch resultB {
case .success(let b):
downloadC(input: b) { resultC in
switch resultC {
case .success(let c):
handler(.success(c))
case .failure(let error):
handler(.failure(error))
}
}
case .failure(let error):
handler(.failure(error))
}
}
case .failure(let error):
handler(.failure(error))
}
}
}
func downloadA(_ handler: @escaping (Result<A, Error>) -> Void) {
}
func downloadB(input: A, _ handler: @escaping (Result<B, Error>) -> Void) {
}
func downloadC(input: B, _ handler: @escaping (Result<C, Error>) -> Void) {
}
The above solution has a couple of issues:
- it is hard to read
- it is error-prone
- lot's of code duplication - handling
success
/failure
cases for every subsequent invocation - it is not Swifty
Can we do better? Let's try to refactor the code using PromiseKit
:
func download() -> Promise<C> {
downloadA()
.chain(with: downloadB)
.chain(with: downloadC)
}
func downloadA() -> Promise<A> {
}
func downloadB(input: A) -> Promise<B> {
}
func downloadC(input: B) -> Promise<C> {
}
Much better! The code is clean and easy to read.
The core idea of Promises is composition. What that means is that if we have a Promise<A>
and a function (A) -> Promise<B>
, we can create a Promise<B>
.
Behind the scenes, Promise automatically handles success
and failure
cases. If at any point in the chain of promises one of them fails, the whole chain fails as well.
This mechanism is encapsulated by the chain
method. Use chain
when you want to invoke another Promise after the current Promise succeeds. Note that the types must match: if you want to chain a Promise<T>
then you must provide a function of type (T) -> Promise<U>
(U can be any type).
Now that we understand the core idea behind promises (which, again, is composability), let's discuss some other abstractions built on top of that.
map
Use map
when you want to transform the (future) value of the promise into some other value.
Example:
Promise<Int>.value(10)
.map { String($0) } // -> transforms into Promise<String>
compactMap
compactMap
is almost identical to map
except that it creates a rejected promise if provided closure returns nil
.
Example:
Promise<String>.value("10")
.compactMap { Int($0) } // -> transforms into Promise<Int>
.then { print($0 } // -> output is "10"
Promise<String>.value("not a number")
.compactMap { Int($0) } // -> transforms into Promise<Int>
.then { print($0 } // -> will not get called!
all
all
lets us group multiple promises into a single promise, which contains the result of all promises.
Example:
let promises: [Promise<String>] = ...
all(promises: promises)
.map { (values: [String]) in ... } // -> do some work on list of Strings
We can combine promises of different types as well:
let p1 = Promise<Int> = ...
let p2 = Promise<String> = ...
all(p1, p2)
.map { (x: Int, y: String) in ... } // -> do some work
any
any
is a dual of all
. It allows us to group multiple promises into a single promise, allowing some (at least one
has to succeed) to fail.
race
race
dispatches a competition between promises, and returns a promise which contains a value of the first promise
that succeeds.
concurrentlyDispatch
concurrentlyDispatch
is an abstraction that allows you to execute tasks concurrently. It can be very usefuly, for example,
when you need to upload a file to a server by splitting it into several smaller chunks:
let file: Data = ...
let chunkSize = 1_000_000 // 1MB
func uploadChunk(_ index: Int) -> Promise<()>? {
let offset = index * chunkSize
guard offset < data.count else { return nil }
let chunk = data[offset..<min(offset + chunkSize, data.count)]
let base64 = chunk.base64EncodedString()
return upload(base64EncodedData: base64)
}
concurrentlyDispatch(
next: uploadChunk,
concurrent: 5, // concurrently upload up to 5 chunks at a time
retryCount: 5 // retry them for a maximum of 5 times in case they fail
)
.finally { print("Upload result: \($0)") }
poll
poll
implements a common server-client communication strategy called polling. You can use it, for instance,
to periodically ping your server:
func checkStatus() -> Promise<Bool> {
// query the server regarding the status ...
}
poll(
repeat: checkStatus, // repeate checking the status
checkAfter: .seconds(5), // ... every 5 seconds
while: { !$0 } // ... while it remains `false`
)
.finally { print("Polling result: \($0)") }
PromiseKit
provides other useful APIs which help us in specific situations. For example, we can directly decode Data
into a Decodable
conforming type:
Promise<Data>.value(json)
.decode(type: Model.self, decoder: JSONDecoder()) // -> produces `Promise<Model>`
When the underlying type of the promise is a Sequence
, we gain additional useful abstractions. In fact, most of the standard higher-order functions you'd expect on sequences are implemented in the PromiseKit
.
Let's take a look at a couple of examples:
// map
Promise<[Int]>.value([1, 2, 3])
.mapValues { $0 * 2 } // -> [2, 4, 6]
// filter
Promise<[Int]>.value([1, 2, 3, 4, 5, 6])
.filterValues { $0 % 2 == 0 } // -> [2, 4, 6]
// reduce
Promise<[Int]>.value([1, 2, 3])
.reduceValues(+) // -> 6
One interesting use-case is when we want to map elements of the sequence into new promises. Use flatMapValues
in such cases:
func fetch(by id: String) -> Promise<Model> { ... }
Promise<[String]>.value(["id1", "id2", "id3"])
.flatMapValues(with: fetch)
.map { (models: [Model]) in ... }
This is a common pattern when developing apps. We first fetch remote API to get a list of items. Then, for every item on the list, we want to fetch another API to get details of an item. With promises, this is a piece of cake.
You can find source code here.