Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Architect Your iOS App for Easy Backend Replacement - Part II: Currying and Partial Application

DZone's Guide to

Architect Your iOS App for Easy Backend Replacement - Part II: Currying and Partial Application

Here is Part II of this series on making your app modular enough so that you can easily switch backends, or even use several different backends at the same time.

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

ausios_comic4

In Part I, we discussed how to architect your iOS app for more modularity and easier backend replaceability. Following the wisdom of John Carmack and Debasish Ghosh, we decided to use plain old functions instead of classes to model behaviors. We stuck those functions inside of protocol extensions. We built each function to accept a repository object, so that the functions are never coupled with any particular backend.

In this part, we’re going to take things further and make our functions more composable so that our functions can be pieced together before evaluating them against a backend.

Let’s re-visit one of the service functions that we constructed in Part 1. The createOrder function models the process of making an order at a coffee shop. It takes two arguments: the first argument is a dictionary of order data, and the second argument is a repository object that represents a backend:

func createOrder(orderInfo:Dictionary, repo:OrderRepository) -> Order {
    let order = Order(orderInfo)
    return repo.write([order]) 
}
let repo = OrderAWSRepository()
let order = createOrder([1:"Latte"], repo)

In the above code, our backend is represented by the OrderAWSRepository. If we want to change the backend from AWS to Firebase we can change the line

let repo = OrderAWSRepository()

to something like this:

let repo = OrderFirebaseRepository()

As we can see, function-based dependency injection helps make it easier to replace backends. But as mentioned in Part I, by ditching classes we’ve now introduced more verbosity into our code, and an even larger problem that I will discuss next.

The Problem With Function-Based Dependency Injection

The problem with the function based dependency injection above is that every single function takes in a repository object. This means that every single function makes a call to the backend. But we may not want to call the backend every single time we call a function!

Returning to our coffee shop example, perhaps we want the user to modify an order using some UI controls, but only send the order through to the backend when a “finalize order” button is pressed.

For example, let’s say we have the following sequence:

  1. Customer ordered coffee
  2. Customer added cake to order
  3. Customer removed cake from order
  4. Order is finalized

We may only want to call the backend in step 4. The first 3 steps should just build up the order pipeline.

How do we architect our functions to accomplish this? In a class/OOP based approach, we could accomplish this by using an instance variable for the repository, and doing the dependency injection at the class level (as we did earlier in Part 1 prior to ditching classes for protocol extensions). But we’re a long way from OOP land. Remember, we only have functions to work with.

To solve the issue, we’re going to employ two functional programming techniques: currying and monadic binding. Let’s start with currying. (If you need a fuller coverage of functional programming in Swift check out the book Functional Programming in Swift , I recommend it.)

Currying

Currying means to transform a function that takes many arguments into a sequence of functions that each accept only one argument. The benefit of currying is that we can use it break up a function into smaller, more modular pieces.

Remember the signature for createOrder?

func createOrder(orderInfo:Dictionary, repo:OrderRepository) -> Order

It looks like this in curried form:

func createOrder(orderInfo:Dictionary) -> OrderRepository -> Order

What the above curried createOrder means is that it will take in a dictionary with the initial order info, and it will return a function that takes in a OrderRepository object as an argument to return an Order object.

The implementation of the curried createOrder function looks like this:

func createOrder(orderInfo:Dictionary) -> OrderRepository -> Order  {
    let order = Order(orderInfo)
    return {repo in repo.write(order)}
}

If we call createOrder using an expression like createOrder([1:"Latte"]), it outputs another function that means “given a repository object as an argument, output an order with a Latte”. We can assign this function to a variable below:

let orderFunc = createOrder([1:"Latte"])

orderFunc is an object of type (OrderRepository -> Order). To get a final Order value, we can pass in a repository object into orderFunc:

let repo = OrderAWSRepository()
let order = orderFunc(repo)

In the above, let order = orderFunc(repo) adds the order to the persistent store (the AWS web service, in our case), and then the order variable is assigned to the new order that is created.

We can also curry our other functions, like addToOrder. Here’s the implementation of our curried addToOrder function, which means “given an Order object as an argument, output a function that, given a repository object as an argument, outputs an order corresponding to orderId, with a new order appended to that order”:

func addToOrder(orderId:Int) -> Order -> OrderRepository -> Order {
    return {order in
        {repo in
            if repo.update(order, orderId: orderId) == true {
                return order
            } else {
                return repo.query(orderId)
            }
        }
    }
}

And here is some code that processes an order using the curried versions of createOrder and addToOrder:

let repo = OrderAWSRepository()
let orderFunc = createOrder([1:"Latte"])
let order = orderFunc(repo)
let orderFunc2 = addToOrder([2:"Cake"])(order)
let order2 = orderFunc2(repo)

So what’s the point of doing all this currying? Currying allows us to generate partially applied functions like orderFunc and orderFunc2 above. In the above code, we evaluate orderFunc and orderFunc2 against the repository to get Order objects, but we didn’t have to. We could have chained the two functions together, and then evaluated the chain as a whole against the repository. Doing so would model the stateful process of “create an order with a latte, then add a cake to the order, then call the backend”. Being able to chain partially applied functions together in that way gives us the terseness, modularity, and “OOP-y statefulness” that we’ve been looking for.

Chaining Partially Applied Functions Together

So how do we actually chain partially applied functions to create a pipeline of “stateful” objects? The answer is the Reader Monad.

Let’s say the UI has an event handler function called finalizeOrderButtonPushed, which is the action that sends through the final order. finalizeOrderButtonPushed accepts an array of partially applied functions, like this:

//We would normally get this from user input events
let actions = [createOrder([1:"Latte"]), addItem([2:"Cake"]), removeItem([2:"Cake"])] 
finalizeOrderButtonPushed(actions)

So how will finalizeOrderButtonPushed take the array of functions and “run” them as if they were done in an ordered sequence? We use the Reader Monad, and that’s what we’ll discuss in our next and final part, Part III. Stay tuned.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
functional programming ,repository ,protocol ,extensions ,dependency injection ,backend

Published at DZone with permission of Moe Burney, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}