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!

Memoize your API calls in Swift using functions' scopes!

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:

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)
	}
}
Yes, it's really just a wrapper around the network call itself

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

Hiking with the kids is always adventure.  But we love to see them take an active part in discovering new things.
Scopes! This brings me back... - Photo by James Lee / Unsplash

How it defines what can be accessed from where, such as how a function cannot access the variables declared inside a nested function...

func outerFunction() {
	func nestedFunction() {
		let a = 1;
	}
	print(a)
}
This will lead to the "Use of unresolved identifier 'a'" error

... but a nested function can access variables and elements declared in outer elements, wether they're a class....

class MyClass {
    var a = 1
    
    func myFunction() {
    	a = 2
        print(a)
    }
}
This is valid...

... a function...

func outerFunction() {
	var a = 1

	func nestedFunction() {
		a = 2
		print(a)
	}
}
... and so is this

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

class MyClass {
	lazy var myFunction: () -> Int = {
		var aVariable = 0
		
		func anotherFunction() -> Int {
			aVariable += 1
			return aVariable
		}
		
		return anotherFunction
	}()
}
This is a stored property, not a computed property. Also, notice the parenthesis at the end?

Anonymous functions

Which can be simplified even more, using an anonymous function!

Pictured - a man at a computer disguised as an anonymous hacker wearing a Guy Fawkes mask.
Is this an Anonymous Function? Sorry, couldn't refrain myself - Photo by Clint Patterson / Unsplash

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:

private var shippingFeesForZipCode: (String) -> Observable<Double?> = {
	var shippingFees: Double? = nil
	var previousZipCode: String? = nil
	return { (zipCode: String) -> Observable<Double?> in
		guard zipCode != previousZipCode else {
			return Observable.just(shippingFees)
		}
		previousZipCode = zipCode
		return Observable.create() { observer in
			NetworkStack.shippingFeesForZipCode(postalCode: zipCode) { (shippingFees, error) in
				guard error == nil else {
					observer.onError(error)
					return
				}
				guard let result = shipping else {
					observer.onError(CustomError)
					return
				}
				shippingFees = result
				observer.onNext(shippingFees)
			}
			return Disposables.create()
		}
	}
}()
Using RxSwift's Observable

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:

private var shippingFeesForZipCode: (String) -> Observable<Double?> = {
	var cachedShippingFees: [String : Double?] = [ : ]
	return { (zipCode: String) -> Observable<Double?> in
		if let shippingFee = cachedShippingFees[zipCode] {
			return Observable.just(shippingFee)
		}
		return Observable.create() { observer in
			NetworkStack.shippingFeesForZipCode(postalCode: zipCode) { (shippingFees, error) in
				guard error == nil else {
					observer.onError(error)
					return
				}
				guard let result = shipping else {
					observer.onError(CustomError)
					return
				}
				cachedShippingFees[zipCode] = shippingFees
				observer.onNext(shippingFees)
			}
			return Disposables.create()
		}
	}
}()
This isn't always necessary, and in some cases it could lead to our cache's size blowing up

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.

"I milked an almond cow" on the side Almond Cow plant-based milk shop in Venice, CA.
Another great quote for the ages - Photo by Daniel Salcius / Unsplash