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.
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.
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.
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
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 😉