API Design — Deriving Future

Måns Bernhardt
May 7, 2018 · 11 min read

In this article, we will explain what asynchronous operations are, why they are useful, and how their APIs are typically designed. Specifically, we will look at an API for performing network requests. Step by step, we will improve this API, first by introducing the type, and later on, by abstracting the asynchronous operation itself, by deriving the type. We will also show how these two types could be implemented and extended with useful operations. In the end, this will lead to code that is more composable, easier to reason about as well as to maintain.

Asynchronous operations

When building applications interacting with a user, a goal is to keep the application responsive at all time. It is thus important to not perform long-running operations that will block the UI. This is one reason why many APIs used in UI intensive applications are asynchronous. Being asynchronous means that the caller will not block while executing an operation using the API, but instead, the API will call the user back when the result is available.

A common reason for an API to be asynchronous is that it models access to external services. These services often end up accessing some hardware. As most hardware have much longer latencies than the main CPU, it does make sense to use the CPU for other work while awaiting the result. External services are also often unreliable by nature, which means that they might fail. So it is common that many asynchronous APIs need to be able to report those failures.

A common asynchronous operation is a network request. On Apple platforms, we typically use 's task-based APIs such as:

The signature of the method hints that it is asynchronous as the expected result is not immediately returned. Instead, a closure is passed to the API. This closure will be called as soon as a result is available or when the operation failed for some reason.

So let us focus in on the . We will ignore the less used to simplify the API a bit. What does it tell us?

We would expect to get some data back or if something went wrong an error. But as a consumer of this API we actually have to handle four cases:

By reading the documentation for we can see that the last two cases should never occur, and it should be safe to ignore them. But it would be great if we did not have to rely on documentation to express this.

Introducing the Result enum

The introduced above originates from Objective-C, and hence is limited to what can be expressed in Objective-C. Swift, on the other hand, has a more powerful type-system making it easier to express many of our ideas in a more type-safe way. One of Swift's constructs are enums with associated values. This is a really powerful construct that would let us express the either or as:

Further, by using Swift generics, can be generalized to work with any success type instead of just :

With at our disposal, we can now simplify our network API. When at it, we will also make the API more focused on the data result than the data task itself:

Transforming results

It is not that common to work directly with an instance of . Typically, we would instead transform the data into more specialized types. In most applications, this would likely be into some JSON container, or into types being deserialized from JSON. Let us investigate how we can transform into JSON.

For this article, we will use the framework Lift to work with JSON. Lift wraps JSON using the container type. It comes with an initializer accepting raw JSON data:

So how do we go about transforming a into a . Firstly, we need a way to extract the success value:

And given a value or a thrown error, we need a way to put them back into a . This is like running in reverse:

Combining the two, allow us to first extract the data from a , then transform it to JSON using , and then finally construct a with the resulting JSON:

This is called a , the operation of transforming a value inside a box into another one:

The implementation of is a great example of how we can compose new methods from more general ones. The function is a very useful function that you will find in other box like types as well, such as Swift's optional and container types. Using , our new JSON method now becomes:

Here we can see how we chained two operations, one asynchronous and one synchronous, both of which might fail. Thanks to and , we could focus on the "happy cases" and avoid writing explicit error handling.

Introducing the Future type

We now have two asynchronous APIs, one returning and the other . As for the asynchronous part of those APIs, the completion function itself, the only thing that differs between the two is the type of their results, the in . It would be nice if we could encapsulate this completion closure in a type and make it generic:

This would let us rewrite as:

To listen on a future’s result, we would just call and pass a completion handler:

This is really powerful. We have abstracted the idea of an asynchronous operation and encapsulated it into a type. Instances of this type could be passed around, this without any need to know the origin of the operation nor having to drag along any of its dependencies.

Having a type representing asynchronous operations also means we can extend it with useful functionality. One of those is , as similar to also acts as a box wrapping a value:

Our JSON method can now be simplified to:

It is now so trivial that it might not even be worth adding a convenience method for it.

Improving the implementation

The current implementation of , although elegantly simple, has some issues. One is, that you have to call on the future to start it. If no one ever calls , its asynchronous operation will never be performed. But the expectation is that it should always perform, even though no one is interested in the result. Some APIs make this explicit by letting the completion handler be optional. You would expect, that e.g. an animation would run even though you passed a nil completion handler.

A related problem is that nothing stops us from calling more than once. This would currently result in a new asynchronous operation being kicked off for each call to . This problem did not exist when we passed a completion handler, where at most one could be passed.

Given the above, it makes sense to model to always execute its work, even though there potentially are no ones interested in the result. We might attempt to restrict the number of listeners to at most one. Potentially, we could assert or complete with an error if calling a second time. But in the context of the user, there might be no way of knowing if somebody is already listening on the future or not. And even if it was, it is not obvious what the user should do with that information. So it also makes sense to allow multiple listeners, but still only execute the future once.

So let us update 's internals by adding an initializer that immediately calls back the provided closure. The previous member now becomes a function instead where we add received completions to a array. As the future now has internal state, we also update it to be a class instead of a struct:

This solves our two original issues but introduces a new one. What if the future has already been completed at the time is called? That would mean that its completion callback will never be called. This will be an issue even if we call immediately after creating the future, as the future could potentially complete even before leaving the initializer:

The solution is to keep the result around in the case someone calls after the future has completed. And once we have a result we do not need to keep the completions around any longer. So the state of a future is now either pending with an array of completions or completed with a result:

The initializer now becomes:

And is updated to immediately call the completion back if the future has already been completed:

Chaining futures

Adding to demonstrated how we can construct a new future from a previous one, in this case by transforming the internal value. A perhaps even more powerful transform is to be able to chain two asynchronous operations. In this case, we want to construct a new future representing one operation performed after another one, where the second operation has access to the result from the first. So we would like a version of where the value is not just transformed into a new one using the transform. Instead, we would like to transform the value into the second operation using a transform. This is called a , and similar to , can be found on Swift optional and containers as well. It would also make sense to add to our type.

To better see how and are basically the same for , , and , we could look at the signatures of their transformation functions:

Given the function signature of , even with the addition of letting the transform to throw errors, the implementation for is still quite straight-forward:

Focus on the “happy flow”

As the transformations passed to both and are called with a success value, we can conclude they will only be called if such a value is available. This normally means that all operations preceding them were successful as well. It is quite typical when working with futures that you focus on this "happy flow". Failures are typically just implicitly forwarded and are explicitly handled only where required, and then normally at the end of a sequence of operations.

Where requires us to handle both failures and successes, it is often more convenient to focus on one or the other. This can easily be improved by adding an helper:

Similarly, we can add an helper.

Bringing it all together we can now build quite complex compositions, such as fetching a user’s friends:

If you wonder what the is all about, it is just how Lift converts the JSON contained inside a into model values.

In this example, you can see how futures makes it easy to chain several transformations after each other. If you would try to do this with the original APIs, using completion callbacks and without , the code would soon be really messy and hard to read and maintain.

In the above example, it might not be obvious at first how much could fail. Both requests could fail, transforming data to JSON could fail, and constructing our model values could fail as well. If all goes well, we end up in 's callback. If something fails, we will end up in 's callback.

Summary

We started out discussing what asynchronous operations are, why they are useful in UI application, and how they typically are modeled with completion callbacks. We then introduced the type to better model asynchronous results. After updating our APIs to use , we recognized that most completion handlers look the same but for the type returned, hence we defined the type to encapsulate asynchronous operations.

We discovered that futures carry a state of being completed or not. This state resulted in the implementation being a bit more tricky then perhaps first anticipated. Finally, we introduced the and transforms and showed how we could chain futures together and mainly focus on the "happy flow".

If you want to learn more about futures and try them out yourself, download the open sourced framework Flow. Here, you can also see how a production implementation might look like. There is also a playground available, where you can see the code introduced in this article all come together.

In an upcoming article, we will see how we can make futures even more powerful and convenient to work with.

iZettle Engineering

We build tools to help business grow — this is how we do it.

Måns Bernhardt

Written by

iOS Developer at iZettle with a focus on frameworks and architecture.

iZettle Engineering

We build tools to help business grow — this is how we do it.