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.
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:
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:
It can be seen that in each of the private methods I have to proxy
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.
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:
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.
As a result, we want to apply both of these operations:
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.
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:
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.
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
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.
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.
We apply the Reducer concept to the existing code - create a RequestState, then initialize it and set it.
For synchronized requests, I added DispatchSemaphore.
Let's move on. Now we need to create a RequestAction with, say, three requests.
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.
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.
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.
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.