Skip to content

Instantly share code, notes, and snippets.

@vincent-pradeilles
Created August 12, 2017 12:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vincent-pradeilles/31de7cdfb63e9485fab2d9dcb45afe0f to your computer and use it in GitHub Desktop.
Save vincent-pradeilles/31de7cdfb63e9485fab2d9dcb45afe0f to your computer and use it in GitHub Desktop.

From async API to Rx API, with little to no work

A demo project that illustrates the content of this article is available on my GitHub.

A very standard pattern in the iOS world to provide data asynchronously is the completionHandler. A service function will take some arguments, perform some work, and then it will provide the result through a completionHandler. Such a service function looks like the following:

https://gist.github.com/9838b23c122ff071f8f44156f90f3177

And its usage would be as follows:

https://gist.github.com/a23b16c3cd26fee3273b8e35577bdcb8

While perfectly sound to use, this approach tends to become tedious when the developer is required to perform requests sequentially, as it leads to nesting callbacks within callbacks, resulting a complex and hard to read code, a situation sometime referred to as "callback hell".

To tackle this issue, libraries such as RxSwift haven been created and they help developers to write asynchronous code in a much more clear and comprehensible fashion.

If you are not familiar with RxSwift, you can read an introduction on its GitHub repository.

The issue at hand

Now let's be realistic, not all code is written using RxSwift, so being able to wrap legacy code is a prerequisite for anyone who wishes to successfully integrate and leverage RxSwift in his project.

A very sound approach to do so would be to use a method from RxSwift that allows to wrap arbitrary code:

https://gist.github.com/2387dfb919a0d83033a99c34d232c124

This sort of wrapping will work perfectly, but the developer will need to each time write a new wrapping method, which is sure to be time consuming. Based on this observation, the ideal would be to have a way to automatically "translate" our legacy API. To achieve this goal, we will first take a detour in the functional programming world.

Currying

In the realm of functional programming, functions are considered like any other primitive type, and this opens the door to some very cool operations, like currying.

Let's consider this function func add(arg1: Int, arg2: Int) -> Int which adds two numbers and returns the result. Usually, to call this function, the developer needs to provide both arguments at the same time:

https://gist.github.com/90247ab1bf1d7181ce2bc9f31af672b1

Curry is an operation that takes a functions and wraps it in a way that allows the developer to provide the arguments one at a time:

https://gist.github.com/45c750544db1d7d1fad0da4348b578b8

When you first look at it, it tends to feel like some kind of black magic, so let's demystify it by looking at the code for currying a function of two arguments:

https://gist.github.com/c100c5b227205dac75cc8d75a542cbc1

What it does is basically wrap the function in a succession of functions that will each receive one of the argument before finally calling the wrapped function. And this behavior is exactly what we will need to achieve our goal of API translation.

The solution

Applying what we have seen so far, we first deal with the base case of functions that only take a completionHandler as argument:

https://gist.github.com/217aa21db7e466073d3927b7f2d8245e

And using curry, we can recourse through the function arguments, until we reach the completionHandler, at which point we are left with the base case that we already know how to handle:

https://gist.github.com/4acd7fbcbdd17734c8bcc149a28fc8ca

Finally, we can use this new function to automatically translate our legacy API like this:

https://gist.github.com/3fdbbc0928c896cb16b1a6daea1e3e2a

Conclusion

Nothing comes for free in this world, and while the technique I've just showed you can enable a significant gain in development time, it has a cost on performances:

  • the use of currying functions can take a significant time to compile, because of all the generics and type inference involved
  • the runtime of the currying process can also be a little long, because it is recursive code which is hard for the compiler to optimize

As far as I am concerned, these drawbacks are definitely not redhibitory, but nevertheless it is important to be aware of them, in order to understand the performance issues that might arise in some specific contexts.

In this article, I have voluntarily not treated the case of service API that also require error handlers. But after understanding how the technique works, I'm sure you'll be able to derive the necessary adaptation to handle this case without problem 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment