Problems Are Solved With Promises

This is a post about the PromiseKit library written as a presentation that goes together with PromiseKit Example.

Federico Mazzini
Major League

--

Why Should We Use Promises?

Let’s take a look at the following example:

static func fetchCharacters(completion: @escaping ([Character]?) -> ()) {
Alamofire.request(charactersURLString).responseData(completionHandler: { response in
if let data = response.data {
do {
let decoder = JSONDecoder()
let characters: [Character] = try decoder.decode([Character].self, from: data)
completion(characters)
}
catch {
completion(nil)
}
}
else {
completion(nil)
}
})
}

What’s Going on There?

A request is made, a GET that’s an asynchronous operation. When an answer is received, a block is executed. Inside this block it is verified if the answer is successful, otherwise an error is printed and the block is exited.

Then, it is verified that the answer is not empty. If it isn’t empty, it is a matter of parsing the response with a do-try-catch for a final validation.

The example is effective, but it is not legible or elegant.

What’s Wrong?

We create a pyramid of keys ({) with many levels where not so much happens, and the most important thing occurs in the last one.

The code has three error verifications, which would increase with each level of complexity that we would like to add.

The example only makes one request, what happens if we want to make more than one? We add another pyramid of closures or write badly optimized code.

Writing Asynchronous Code with Promises

return firstly {
Alamofire.request(charactersURLString, method: .get).responseData()
}.map { data, rsp in
let decoder = JSONDecoder()
let characters: [Character] = try decoder.decode([Character].self, from: data)
return characters
}

Better? This is What Happened:

The decode function uses a try, but there is no do-catch! The promise keeps the error everywhere it can happen and then handles it with another operator: catch.

Instead of defining a completion handler, the response is chained to another PromiseKit operator that also returns a promise: .map. Chaining operations that generate promises is typical of PromiseKit, and there are many options.

Besides PromiseKit we use a pod called PromiseKit/Alamofire, that includes promises that are optimized to accomplish Alamofire tasks.

Finally, we can use our function wherever we need, as follows:

firstly {
Webservice.fetchCharacters()
}.done { characters in
self.characters = characters
self.tableView.reloadData()
}.catch { error in
print(error)
}

But How Can I Create and Take Advantage of My Own Promises?

Sometimes called Futures, Delay, or Deferred in other languages, promises are built to handle asynchronous tasks that may or may not succeed. Unlike closures, promises are designed to be passed on from one place to another without having to capture information from the context where they were generated.

The design works by wrapping asynchronous functions with an object that only exposes the value they promise, and communicates through a block called .done {}

To create a promise, we use the following constructor:

Promise<T> { seal in
// seal.fulfill(foo)
// seal.reject(error)
// seal.resolve(foo, error)
}

Let’s work with an example to understand how it works. Next, we’ll try to write text in a file:

class FileService {

// File writing.
static func write(text: String, toFile filename: String, completion: @escaping (NSError?) -> ()) {
let err = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error writing to file."])

guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion(err)
return
}

let fileURL = dir.appendingPathComponent(filename)

// Writing.
do {
try text.write(to: fileURL, atomically: false, encoding: .utf8)
completion(nil)
}
catch {
completion(err)
}
}
//…

If we wanted to create a promise with that asynchronous operation, we would use the constructor like this:

return Promise<Void> { seal in
FileService.write(text: text, toFile: filename, completion: { (error) in
if let err = error {
seal.reject(err)
}
else {
seal.fulfill(())
}
})
}

This how we use the constructor to wrap our operation: we must specify the value for its success and the value for its failure. Always specifying both reject and fulfill. And the return value which, in this example, is almost none so we wrote Promise <Void>.

Note that () is the only Void type value. That’s why we write fulfill (()).

We could also build a promise to read from the file. If we had to implement them in a chained way, the result would be the following:

firstly {
FileService.write(text: "hello", toFile: "hello.txt")
}.then {
FileService.read(file: "hello.txt")
}.done { fileContent in
print(fileContent)
}.catch { error in
print(error)
}

Here we see a new block of PromiseKit in action, .then {}. Finally, I compiled some examples of this block as well as the most used blocks.

Most Used Blocks

.then and .done:

.then is used to structure our chains of promises and make them easy to read. One successful promise leads to another, line by line, using .then:

firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}

.done is the same as .then but is does not give promises back, it’s the end of the successful part of our chain. Its use is mandatory in the implementation of many wrappers such as those included in PromiseKit / Alamofire.

.ensure:

No matter the result of our chain, the .ensure block is always called.

firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
//…
}

.when:

.when is one of the most valuable Promisekit functions, it serves to handle parallelism, and comes in two different flavors: when (fulfilled :) and .race.

Without promises, handling parallel processes might be unclear:

var result1: …!
var result2: …!
let group = DispatchGroup()
group.enter()
group.enter()
operation1 {
result1 = $0
group.leave()
}
operation2 {
result2 = $0
group.leave()
}
group.notify(queue: .main) {
finish(result1, result2)
}

Promises make everything easier:

firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//…
}

.when, by default, is used with the function when(fulfilled :), it waits for all the promises that we pass as parameters and, if any of them fails, the chain is rejected.

.race

What if we want to establish a race between parallel processes? Our code can become even less clear. The .race block establishes this race: the promise that returns first becomes the result.

let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)
race(when(fulfilled: fetches).asVoid(), timeout).then {
//…
}

You have to make sure that the promises return the same type. The easiest way to make sure of that is to use asVoid ().

Note that if any of the promises fails, the race also fails. That is to say: everything has to return successfully. Regardless of whether we only use the first result.

Hope you liked this post. Stay tuned for more! If you have any comment our suggestion, leave it below!

Are you a Developer or Designer looking for challenging new projects? Are you looking for Developers or Designers? Major League is the right fit for you!

--

--