How to work with multiple queries. Composition, Reducer, FP

Hi, Habr. My name is Maxim, I am an iOS developer at FINCH. Today I will show you some practices of using functional programming that we have developed in our department.

I want to note right away that I do not urge you to use functional programming everywhere - this is not a panacea for all problems. But it seems to me, in some cases, FP can provide the most flexible and elegant solutions to non-standard problems.

FP is a popular concept, so I will not explain the basics. I am sure that you already use map, reduce, compactMap, first (where :) and similar technologies in your projects. The article will focus on solving the problem of multiple queries and working with reducer.

Multiple Query Issue


I work in outsourcing production, and there are situations when a client with his subcontractors takes care of creating a backend. This is far from the most convenient backend and you have to make multiple and parallel queries.

Sometimes I could write something like:

networkClient.sendRequest(request1) { result in
    switch result {
    case .success(let response1):
                // ...
        self.networkClient.sendRequest(request2) { result in
                    // ...
             switch result {
             case .success(let response2):
                   // ... что - то делаем со вторым response 
                   self.networkClient.sendRequest(request3) { result in
                       switch result {
                       case .success(let response3):
                           // ... тут что-то делаем с конечным результатом
                           completion(Result.success(response3))
                       case .failure(let error):
                           completion(Result.failure(.description(error)))
                       }
                   }
             case .failure(let error):
                 completionHandler(Result.failure(.description(error)))
             }
         }
    case .failure(let error):
        completionHandler(Result.failure(.description(error)))
    }
}

Disgusting, right? But this is the reality with which I needed to work.

I needed to send three consecutive requests for authorization. During refactoring, I thought it would be a good idea to split each request into separate methods and call them inside completion, thereby unloading one huge method. It turned out something like:


func obtainUserStatus(completion: @escaping (Result) -> Void) {
        let endpoint= AuthEndpoint.loginRoute
        networkService.request(endpoint: endpoint, cachingEnabled: false) { [weak self] (result: Result) in
            switch result {
            case .success(let response):
                self?.obtainLoginResponse(response: response, completion: completion)
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    private func obtainLoginResponse(_ response: LoginRouteResponse, completion: @escaping (Result) -> Void) {
       let endpoint= AuthEndpoint.login
        networkService.request(endpoint: endpoint, cachingEnabled: false) { [weak self] (result: Result) in
            switch result {
            case .success(let response):
                self?.obtainAuthResponse(response: response, completion: completion)
            case .failure(let error):
                completion(.failure(error))
            }
        }
private func obtainAuthResponse(_ response: LoginResponse, completion: @escaping (Result) -> Void) {
       let endpoint= AuthEndpoint.auth
        networkService.request(endpoint: endpoint, cachingEnabled: false) { (result: Result) in
    	completion(result)
        }
}

It can be seen that in each of the private methods I have to proxy

completion: @escaping (Result) -> Void 

and I don’t really like it.

Then the thought came to my mind - “why not resort to functional programming?” In addition, swift, with its magic and syntactic sugar, makes it possible to break code into individual elements in an interesting and digestible way.

Composition and Reducer


Functional programming is closely related to the concept of composition - mixing, combining something. In functional programming, composition suggests that we combine behavior from individual blocks, and then, in the future, work with it.

Composition from a mathematical point of view is something like:

func compose(_ f: @escaping (A) -> B, and g: @escaping (B) -> C) -> (A) -> C {
    return { a in g(f(a)) }
}

There are functions f and g that internally specify output and input parameters. We want to get some kind of resulting behavior from these input methods.

As an example, you can make two closure, one of which increases the input number by 1, and the second multiplies by itself.

let increment: (Int) -> Int = { value in
    return value + 1
}
let multiply: (Int) -> Int = { value in
    return value * value
}

As a result, we want to apply both of these operations:


let result = compose(multiply, and: increment)
result(10) // в результате имеем число 101


Unfortunately, my example is not associative
(if we swap increment and multiply, we get the number 121), but for now let’s omit this moment.


let result = compose(increment, and: multiply)
result(10) // в результате имеем число 121

PS I specifically try to make my examples simpler so that it is as clear as possible)

In practice, you often need to do something like this:


let value: Int? = array
        .lazy
        .filter { $0 % 2 == 1 }
        .first(where: { $0 > 10 })

This is the composition. We set the input action and get some output effect. But this is not just the addition of some objects - this is the addition of a whole behavior.

And now let's think more abstractly :)


In our application, we have some kind of state. This may be the screen that the user currently sees or the current data that is stored in the application, etc.
In addition, we have action - this is the action that the user can do (click on the button, scroll through the collection, close the application, etc.). As a result, we operate on these two concepts and connect them with each other, that is, we combine, hmmm we combine (somewhere I heard it before).

But what if you create an entity that just combines my state and action together?

So we get Reducer


struct Reducer {
    let reduce: (S, A) -> S
}

We will give the current state and action to the reduce method input, and at the output we will get a new state, which was formed inside reduce.

We can describe this structure in several ways: by defining a new state, using a functional method or using mutable models.


struct Reducer {
    let reduce: (S, A) -> S
}
struct Reducer {
    let reduce: (S) -> (A) -> S
}
struct Reducer {
    let reduce: (inout S, A) -> Void
}

The first option is "classic."

The second is more functional. The point is that we are not returning state, but a method that takes action, which already in turn returns state. This is essentially the curry of the reduce method.

The third option is to work with state by reference. With this approach, we do not just issue state, but work with a reference to the object that comes in. It seems to me that this method is not very good, because such (mutable) models are bad. It is better to rebuild the new state (instance) and return it. But for simplicity and demonstration of further examples, we agree to use the latter option.

Applying reducer


We apply the Reducer concept to the existing code - create a RequestState, then initialize it and set it.


class RequestState {
    // MARK: - Private properties
    private let semaphore = DispatchSemaphore(value: 0)
    private let networkClient: NetworkClient = NetworkClientImp()
    // MARK: - Public methods
    func sendRequest(_ request: RequestProtocol, completion: ((Result) -> Void)?) {
        networkClient.sendRequest(request) { (result: Result) in
            completion?(result)   
            self.semaphore.signal()
        }
       semaphore.wait()
    }
}

For synchronized requests, I added DispatchSemaphore.

Let's move on. Now we need to create a RequestAction with, say, three requests.


enum RequestAction {
    case sendFirstRequest(FirstRequest)
    case sendSecondRequest(SecondRequest)
    case sendThirdRequest(ThirdRequest)
}

Now create a Reducer that has a RequestState and RequestAction. We set the behavior - what do we want to do with the first, second, third request.


let requestReducer = Reducer { state, action in
    switch action {
    case .sendFirstRequest(let request):
        state.sendRequest(request) { (result: Result) in
            // 1 Response
        }
    case .sendSecondRequest(let request):
        state.sendRequest(request) { (result: Result) in
            // 2 Response
        }
    case .sendThirdRequest(let request):
        state.sendRequest(request) { (result: Result) in
            // 3 Response
        }
    }
}

In the end, we call these methods. It turns out a more declarative style, in which it is clear that the first, second and third requests are coming. Everything is readable and clear.


var state = RequestState()
requestReducer.reduce(&state, .sendFirstRequest(FirstRequest()))
requestReducer.reduce(&state, .sendSecondRequest(SecondRequest()))
requestReducer.reduce(&state, .sendThirdRequest(ThirdRequest()))

Conclusion


Don't be afraid to learn new things and don't be afraid to learn functional programming. I think the best practices are at the crossroads of technology. Try to combine and take better from different programming paradigms.

If there is any non-trivial task, then it makes sense to look at it from a different angle.

Also popular now: