Memoize your API calls in Swift using functions' scopes!
Improve your iOS app's performance and battery usage by not repeating expensive computation and network calls, using memoization, in Swift!
Wait. Memo-what?
Memoization. It's a weird sounding unfamiliar name for something both intuitive, straightforward and incredibly useful: storing and recalling expensive functions calls' results instead of computing them each and every time. Rings any bell? Yes! Caching!
Cue one of my favorite CS quotes!
There are only two hard things in Computer Science: cache invalidation and naming things. - Phil Karlton
Thankfully, in our case, cache invalidation will be quite easy. The cache will simply go away when we close the view. Anyway, if you are in a hurry, here's a link straight to the end result.
Initial situation
We have a delivery app. Shipping fees are based on the delivery location's zip code. We'll keep it simple and assume two things: shipping fees for a specific zip code may change every once in a while, but they aren't and aren't required to be highly dynamic, and our user is a new user.
Here's what the function getting those shipping fees could look like:
He or she enter the delivery address. We make a call to the server, get the shipping fees for that address' zipcode.
Then he or she notices a typo in the adress. They fix it. We make another call to the server and get, again, the shipping fees for the same zip code.
Having fixed the typo, the user decides to instead have it shipped to his / her office, in another town, for convenience. He or she inputs the new address. We make a third call to the server, and this time get the shipping from a second zip code.
Finally, the user having added a cumbersome object to his or her cart, he or she reverts back to the inital delivery address. We, once more, make a fourth call to the server to get the shipping fees, and it's the third time we're doing that for this zip code. I really hope our server isn't too slow, and the cellular connection better be good enough.
If this sounds like a waste to you, well, it is. And sure, we can rely on server caching and network layer caching rules, but we don't always have a say in either the server's rules (when using a third-party, sometimes expensive, API), or what will be used as caching keys.
Let's memoize our shipping fees!
First, we could simply store the previous zip code, along with it's shipping fees. You've probably done something similar before, at one point or another, storing these values in the ViewController directly. It would have looked like that
class MyViewController: UIViewController {
var shippingFees: Double? = nil
var previousZipCode: String? = nil
// ... Some other properties and some code
private func shippingFeesForZipCode(zipCode: String, callback: (Double?, Error?) -> Void) {
NetworkStack.shippingFeesForZipCode(postalCode: zipCode) { (shippingFees, error) in
guard error == nil else {
callback(nil, error)
return
}
guard let result = shipping else {
callback(nil, CustomError)
return
}
callback(result, nil)
}
}
}
This allows us to be sure of our cache's size (2 string variables), and it's quite simple to set up. But it quickly makes for a cluttered ViewController (or ViewModel, or whatever. The architecture isn't relevant here) as we add more and more variables and state to our that isn't relevant to anything beyond a single function. How do we avoid that? Let's play around using functions scopes! If you are famliliar enough with scoping and anonymous functions, skip ahead to how we can apply those to solve our problem.
Required concepts
Scope
As a developer, we all know about scope (if you are unfamiliar with that, here's a nice article, and here's a more formal one).
How it defines what can be accessed from where, such as how a function cannot access the variables declared inside a nested function...
... but a nested function can access variables and elements declared in outer elements, wether they're a class....
... a function...
... or anything else, really.
How does this help us? Well, a function can "capture" it's scope. So if it uses variables defined in it's outer scope, it will keep them and be able to use them, no matter where it is used. See here for more explainations :-)
So if you have a function...
func aFunction() {
}
... that declares a variable...
func aFunction() {
var aVariable = 0
}
... and returns another, nested function...
func aFunction() -> () {
var aVariable = 0
func anotherFunction() {
}
return anotherFunction
}
... which uses said variable...
func aFunction() -> () -> Int {
var aVariable = 0
func anotherFunction() {
aVariable += 1
return aVariable
}
return anotherFunction
}
Then you can effectively retain something, a value, between each of the inner function's execution!
func aFunction() -> () -> Int {
var aVariable = 0
func anotherFunction() -> Int {
aVariable += 1
return aVariable
}
return anotherFunction
}
let anotherFunction = aFunction()
anotherFunction() // returns 1
anotherFunction() // returns 2
anotherFunction() // returns 3
Let's make it nicer to use, using lazily assigned properties...
Now, this is interesting, but we don't want to have to assign our function to some locally scoped variable somewhere. We'd like to be able to just call it. So we'll just directly assign it to one of class' properties, and have it lazily assigned on it's first call, like so:
Anonymous functions
Which can be simplified even more, using an anonymous function!
Which, yes, incidentally we've already used in our variable's assignment (it's, the { ... some code... }()
we use as the property's value)
class MyClass {
lazy var myFunction: () -> Int = {
var aVariable = 0
return {
aVariable += 1
return aVariable
}
}()
}
Great! Let's put it to use!
Solution
private lazy var shippingFeesForZipCode: (String, ((Double?, Error?) -> Void)) -> Void = {
var shippingFees: Double? = nil
var previousZipCode: String? = nil
return { (zipCode: String, callback: ((Double?, Error?) -> Void)) in
guard zipCode != previousZipCode else {
callback(shippingFees, nil)
return
}
previousZipCode = zipCode
NetworkStack.shippingFeesForZipCode(postalCode: zipCode) { (shippingFees, error) in
guard error == nil else {
callback(nil, error)
return
}
guard let result = shipping else {
callback(nil, CustomError)
return
}
shippingFees = result
callback(shippingFees, nil)
}
}
}()
As you can see, this is entirely self-contained. No more storing values in the class itself, or elsewhere. There are, however, two main drawbacks.
First, our function now has to be declared as a variable. This doesn't change much what it is, but it can take some of your colleagues by surprise. And perhaps confuse them. So can the variable's type, which now serves as our former function's signature and can be trickier to write down.
Second, it is a bit trickier to write down. We have to figure out the variable's type, on our own, properly scope our calls, and remember to finish our variable's definition by executing the anonymous functions that gives it it's value! Nothing hard, per se, but unusual nonetheless. A new habit to form.
Now, where would we put this new function? Well, it depends on your own architectural taste and preferences. In my case, it went into my view's ViewModel, meaning all stored values are gone with it when the view is dismissed.
Improvements
Simplify the signature by removing the callback parameter
We can get rid of the need to pass a callback using RxSwift, PromiseKit or Combine, which would look like this:
Cache more shipping fees using a dictionary
Instead of storing the previous zip code and it's associated shipping fee as two variables, we could store multiple zip codes' shipping fees using a dictionary, like this:
Epilogue
Using scope to store and cache values is a neat trick. Even if it does require a bit rethinking, may be disturbing to a few people at first, and might necessitate some explaining, it can help us drastically declutter our classes and structures, making them simpler and cleaner. Definitely worth it.
Comments ()