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:
And its usage would be as follows:
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 in a complex and hard to read code, a situation sometime referred to as “callback hell”.
To tackle this issue, libraries such as RxSwift have been created and they help developers write asynchronous code in a much clearer and more 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:
This sort of wrapping will work perfectly, but the developer will need to each time write a new wrapping method, which is sure to prove 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.
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:
Curry is an operation that takes a function and wraps it in a way that allows to provide the arguments one at a time:
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:
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.
Applying what we have seen so far, we first deal with the base case of functions that only take a
completionHandler as argument:
And using curry, we can recurse 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:
Finally, we can use this new function to automatically translate our legacy API like this:
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 adaptations to handle this case without problem 😉
Originally published at gist.github.com.