ABOUT
TEAM
WORK
CONTACT
|
BLOG

GirAppe blog

2016/04/20 - go to articles list

Promises, unravelling the spaghetti code

It is nice to write clean code as it's both easy to understand and easy to maintain. If I were to define "clean code" in modern programming means, I'll say:

This is quite simple for basic apps and processes, but the bigger it grows, the harder it is to maintain readability and clarity, and the need for some additional level of abstraction grows.

This is easily achieved by the Promises. For the sake of this post, I'll stick to PromiseKit implementation.

The tale of Promises and Futures

Let's start with some historical facts. The terms Promise and Future originate somewhere in the late 1970s. Wiki indicates that Promise was first used by Daniel Friedman and David Wise in a paper in '76, and a similar concept of Future was introduced in '77 by Henry Baker and Carl Hewitt.

The terms were applied mostly to functional programming first, and hence goes its first implementations in the 80's, for somehow "exotic" languages like MultiLisp or Act 1. It served its purpose for functional and parallel programming pretty well, being known to few.

Then the "promise revolution" started around 2000. Most sources claim that it originated in Java Script, pointing to its usability in responsiveness and the request-response model. Whatever the root cause was - it spread onto several mainstream languages, including java, python, C++ and C#. When I’m writing this, at least 20 of so called "main" languages adopt promises natively, with numbers of external libraries available for others, and Promises pattern, from an exotic curiosity, became an equal partner, and a solid foundation for many applications.

What are Promises and Futures?

Let's start with the first sentence in the first chapter of PromiseKit documentation:

"A promise represents the future value of an asynchronous task."

That's pretty it. To understand it intuitively let's think for a moment about it in a similar way as we think about lazy instantiated values. Let's consider:

lazy var userImage: UIImage = {
  if let image = self.user.image {
    return image
  }
  else {
    return UIImage(named: "placeholder")!
  }
}()

This is one of the possible usages of lazy instantiated values. Let assume the following example:

  ...
  self.imageView.image = self.userImage
  ...

At the very moment we call its value, the userImage is be evaluated. If the user has an image set - then we get it, otherwise we use some placeholder. The power of lazy instantiation lies in evaluating things only when needed.

But it has some flaws also - it's synchronous, which means, we can't make this:

lazy var userImage: UIImage = {
  if let image = self.user.image {
    return image
  }
  else {
    // Show image picker, allow user to pick an image
    return // picked image
  }
}()

That's too bad, as sometimes this could be very handy. And that's where Promises jump in.

Let's think about promise as an async lazy value - it will eventually evaluate to a value (or not - and return an error), and we can plan, what to do with it. It is an object - so we can store it, save it, and do all the things we usually do with objects.

In that case, setting user image could look like this:

lazy var promiseImage: Promise<UIImage> = ... // Promise that uses existing image or shows picker etc.

...
promiseImage
.then { image -> Void in
  self.imageView.image = image
}
.error { error in
  self.handleErrorAppriopriately(error)
}
...

Handling a Promise means calling its methods - and that's how we specify what happens when its value finally evaluates. Calling "then", we specify block to handle value evaluation. Let's note that since we can call it more than once, it behaves like multiple completion blocks (called in order)!

Note: Chaining completion blocks is possible, because most of the promise methods (like "then", "always", "recover") return promises as well. That's pretty convenient, and allows to keep the code nice and clean.

From the Promise perspective, the "magic" code looks like that:

lazy var promiseImage: Promise<UIImage> = {
  return Promise<UIImage>(resolvers: { fulfill, reject in
    if let image = self.user.image {
      fulfill(image)
    }
    else {
      // Show image picker, allow user to pick an image
      // when got image from picker
      if success {
        fulfill(image)
      }
      else {
        reject(error)
      }
    }
  })
}

Promise is an object we can fulfill - which indicates success (value got evaluated), or reject - which indicates failure. From the app logic perspective, we operate on its future value, specifying what to do with it.

Note. Future is (in some implementations) the placeholder for Promise value. In PromiseKit Promise object serves both as Promise (it can be resolved) and Future (allows to call methods like "then" or "error" to specify its behaviour)

To sum up the whole intuitive approach to promises:

Promises - (un)chained

One of the greatest features of Promises is their chainability. They are a great alternative for somehow typical completionBlock approach, and it is what does most of the unravelling. Let's consider the following use case:

  1. Authenticate user on some backend with given credentials (get a token)
  2. Get a user profile, using this token
  3. Download a user image from url specified in the profile, or use placeholder if none
  4. Set imageView image with the given image or placeholder

A typical, completion block based approach, will look something like this:

func getUserData() {
  let user = User(email: "user@email.com", password: "...")

  // 1) Log in
  API.login(user, success: { authenticatedUser in
    // 2) Profile
    API.getProfile(authenticatedUser, success: { profile in
      // 3) Try image
      if let imageUrl = profile.photoUrl {
        API.downloadImage(imageUrl, success: { image in
          // 4A) Set image
          self.imageView.image = image
        }, failure: {
          // handle error
        })
      }
      // 4B) Set image
      else {
        self.imageView.image = UIImage(named: "placeholder")
      }
    }, failure: { error in
      // handle error
    })
  }, failure: { error in
    // handle error
  })
}

As we see, the whole process consists of 4 parts, nested in each other success blocks. This is not very readable. Also, we have to handle error in at least 3 different places (imagine making changes in some bigger operation!), and, as well, handling resolved image (either downloaded or placeholder) is also done in several places (4A and 4B).

Let's now check what would it look like with a promise pattern:

func getUserData() {
  let user = User(email: "user@email.com", password: "...")

  // 1) Log in
  API.promiseLogin(user)       // returns Promise<AuthenticatedUser>
  .then(API.promiseProfile)    // API.promiseProfile is a method: AuthenticatedUser -> Promise<UserProfile>
  .then { (profile: UserProfile) -> Promise<UIImage> in
    return API.promiseImage(profile.photoUrl) // if photoUrl is nil, it will immediately fail
  }
  .recover { error -> UIImage
    return UIImage(named: "placeholder") // if it failed, we use placeholder
  }
  .then { image -> Void in
    self.imageView.image = image
  }
  .error {
    // handle error
  }
}

In this case the whole process is modular -- we specify its parts (promises for login, profile, image), and then we compose them into more complex behaviour. WWith this approach, we can clearly see what comes first, what next, and the whole thing becomes flat (no more "pyramid of doom").

PromiseKit itself contains (in an ALAssetsLibrary) a lot of ready-to-use promises, for the most of common things, but we can, as well, prepare our own.

With this kind of approach, we can create complex behaviours, compositing them from parts, allowing us to focus on the business logic, not implementation itself. Every task could be treated and wrapped as an async task, allowing work on desired abstract level.

The promiseLogin above could be changed, so that it covers: showing login screen, singing up etc., and the core logic remains unchanged. After all, at this point, we don't care about details of implementation --we need an authenticated user, and what to do with it.

Promises definitely changed the way I think about development, and I enjoy it a lot.

Andrzej Michnia
Software Developer