📜 ⬆️ ⬇️

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

Hi, Habr. My name is Maxim, I am an iOS developer at FINCH. Today I will show you some of the functional programming practices 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 that in some cases, the OP can provide the most flexible and elegant solutions to non-standard tasks.

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

The problem of multiple queries


I work in outsourcing production, and there are situations when a client with his subcontractors takes over the creation of a backend. This is not the most convenient backend and you have to do multiple and parallel requests.
')
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 had to send three consecutive requests for authorization. During refactoring, I thought it would be a good idea to break up each request for individual methods and call them inside the completion, thereby unloading one huge method. It turned out something like:

 func obtainUserStatus(completion: @escaping (Result<AuthResponse>) -> Void) { let endpoint= AuthEndpoint.loginRoute networkService.request(endpoint: endpoint, cachingEnabled: false) { [weak self] (result: Result<LoginRouteResponse>) 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<AuthResponse>) -> Void) { let endpoint= AuthEndpoint.login networkService.request(endpoint: endpoint, cachingEnabled: false) { [weak self] (result: Result<LoginResponse>) 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<AuthResponse>) -> Void) { let endpoint= AuthEndpoint.auth networkService.request(endpoint: endpoint, cachingEnabled: false) { (result: Result<AuthResponse>) in completion(result) } } 

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

 completion: @escaping (Result<AuthResponse>) -> Void 

and I do not really like it.

Then the thought came to me - “Why not resort to functional programming?” Besides, the swift, with its magic and syntactic sugar, makes it interesting and convenient to break the code into separate elements.

Composition and Reducer


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

Composition from a mathematical point of view is something like:

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

There are functions f and g, which internally define the output and input parameters. We want to get some resultant 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 the increment and multiply, then we get the number 121), but for now let us 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 action. But this is not just the addition of some objects - this is the addition of a whole behavior.

And now let's think more abstract :)


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

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

So we get a Reducer

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

At the input of the reduce method, we will give the current state and action, and at the output we will get a new state that was formed inside the reduce.

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

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

The first option is "classic".

The second is more functional. The point is that we do not return a state, but a method that accepts action, which in turn returns a state. In essence, this is a currying of the reduce method.

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

Application reducer


Let's apply the Reducer concept to the existing code - create 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<Response: Codable>(_ request: RequestProtocol, completion: ((Result<Response>) -> Void)?) { networkClient.sendRequest(request) { (result: Result<Response>) in completion?(result) self.semaphore.signal() } semaphore.wait() } } 

For query synchronization, I added the DispatchSemaphore

Go ahead. Now we need to create RequestAction with, say, three requests.

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

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

 let requestReducer = Reducer<RequestState, RequestAction> { state, action in switch action { case .sendFirstRequest(let request): state.sendRequest(request) { (result: Result<FirstResponse>) in // 1 Response } case .sendSecondRequest(let request): state.sendRequest(request) { (result: Result<SecondResponse>) in // 2 Response } case .sendThirdRequest(let request): state.sendRequest(request) { (result: Result<ThirdResponse>) 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 being made. Everything is readable and clearly.

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

Conclusion


Do not be afraid to learn new things and do not be afraid to learn functional programming. I think that the best practices are at the junction of technology. Try to combine and take better from different programming paradigms.

If there is some nontrivial task, then it makes sense to look at it from a different angle.

Source: https://habr.com/ru/post/455840/


All Articles