Oi Habr. Meu nome é Maxim, sou desenvolvedor iOS da FINCH. Hoje vou mostrar algumas práticas de uso de programação funcional que desenvolvemos em nosso departamento.
Quero notar imediatamente que não recomendo que você use a programação funcional em qualquer lugar - isso não é uma panacéia para todos os problemas. Mas parece-me que, em alguns casos, o FP pode fornecer as soluções mais flexíveis e elegantes para problemas fora do padrão.
FP é um conceito popular, então não vou explicar o básico. Estou certo de que você já usa map, reduza, compactMap, primeiro (onde :) e tecnologias similares em seus projetos. O artigo se concentrará em resolver o problema de várias consultas e trabalhar com redutor.
Problema com várias consultas
Trabalho na terceirização da produção e há situações em que um cliente com seus subcontratados cuida da criação de um back-end. Isso está longe de ser o back-end mais conveniente e você precisa fazer várias consultas paralelas.
Às vezes eu poderia escrever algo como:
networkClient.sendRequest(request1) { result in switch result { case .success(let response1):
Nojento, certo? Mas essa é a realidade com a qual eu precisava trabalhar.
Eu precisava enviar três pedidos consecutivos de autorização. Durante a refatoração, pensei que seria uma boa idéia dividir cada solicitação em métodos separados e chamá-los de conclusão, descarregando assim um método enorme. Aconteceu algo como:
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) } }
Pode-se ver que em cada um dos métodos privados eu tenho que proxy
completion: @escaping (Result<AuthResponse>) -> Void
e eu realmente não gosto disso.
Então, o pensamento veio à minha mente - "por que não recorrer à programação funcional?" Além disso, o swift, com seu açúcar mágico e sintático, torna possível dividir o código em elementos individuais de uma maneira interessante e digerível.
Composição e Redutor
A programação funcional está intimamente relacionada ao conceito de composição - mistura, combinação de algo. Na programação funcional, a composição sugere que combinemos o comportamento de blocos individuais e depois, no futuro, trabalhemos com ele.
A composição do ponto de vista matemático é algo como:
func compose<A,B,C>(_ f: @escaping (A) -> B, and g: @escaping (B) -> C) -> (A) -> C { return { a in g(f(a)) } }
Existem funções fe que especificam internamente os parâmetros de saída e entrada. Queremos obter algum tipo de comportamento resultante desses métodos de entrada.
Como exemplo, você pode fazer dois fechamentos, um dos quais aumenta o número de entrada em 1 e o segundo se multiplica por si só.
let increment: (Int) -> Int = { value in return value + 1 } let multiply: (Int) -> Int = { value in return value * value }
Como resultado, queremos aplicar as duas operações:
let result = compose(multiply, and: increment) result(10)
Infelizmente meu exemplo não é associativo
(se trocarmos incremento e multiplicação, obtemos o número 121), mas por enquanto vamos omitir esse momento.
let result = compose(increment, and: multiply) result(10)
PS: Eu tento especificamente tornar meus exemplos mais simples, para que fiquem o mais claro possível)
Na prática, você geralmente precisa fazer algo assim:
let value: Int? = array .lazy .filter { $0 % 2 == 1 } .first(where: { $0 > 10 })
Essa é a composição. Definimos a ação de entrada e obtemos algum efeito de saída. Mas isso não é apenas a adição de alguns objetos - é a adição de todo um comportamento.
E agora vamos pensar mais abstratamente :)
Em nossa aplicação, temos algum tipo de estado. Pode ser a tela que o usuário vê atualmente ou os dados atuais armazenados no aplicativo, etc.
Além disso, temos ação - esta é a ação que o usuário pode executar (clique no botão, role pela coleção, feche o aplicativo etc.). Como resultado, operamos sobre esses dois conceitos e os conectamos um ao outro, ou seja, combinamos, hummm que combinamos (em algum lugar que eu já ouvi isso antes).
Mas e se você criar uma entidade que apenas combine meu estado e ação?
Então temos Redutor
struct Reducer<S, A> { let reduce: (S, A) -> S }
Daremos o estado atual e a ação à entrada do método de redução e, na saída, obteremos um novo estado, que foi formado dentro de reduzir.
Podemos descrever essa estrutura de várias maneiras: definindo um novo estado, usando um método funcional ou usando modelos mutáveis.
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 }
A primeira opção é "clássica".
O segundo é mais funcional. O ponto é que não estamos retornando estado, mas um método que executa uma ação, que por sua vez já retorna estado. Este é essencialmente o curry do método de redução.
A terceira opção é trabalhar com o estado por referência. Com essa abordagem, não apenas emitimos estado, mas trabalhamos com uma referência ao objeto que entra. Parece-me que esse método não é muito bom, porque esses modelos (mutáveis) são ruins. É melhor reconstruir o novo estado (instância) e devolvê-lo. Mas, para simplificar e demonstrar mais exemplos, concordamos em usar a última opção.
Aplicando redutor
Aplicamos o conceito Redutor ao código existente - crie um RequestState, inicialize-o e defina-o.
class RequestState {
Para sincronização de solicitações, adicionei DispatchSemaphore
Vá em frente. Agora, precisamos criar uma RequestAction com, digamos, três solicitações.
enum RequestAction { case sendFirstRequest(FirstRequest) case sendSecondRequest(SecondRequest) case sendThirdRequest(ThirdRequest) }
Agora crie um redutor que tenha um RequestState e RequestAction. Definimos o comportamento - o que queremos fazer com o primeiro, segundo e terceiro pedidos.
let requestReducer = Reducer<RequestState, RequestAction> { state, action in switch action { case .sendFirstRequest(let request): state.sendRequest(request) { (result: Result<FirstResponse>) in
No final, chamamos esses métodos. Acontece um estilo mais declarativo, no qual fica claro que os primeiro, segundo e terceiro pedidos estão chegando. Tudo é legível e claro.
var state = RequestState() requestReducer.reduce(&state, .sendFirstRequest(FirstRequest())) requestReducer.reduce(&state, .sendSecondRequest(SecondRequest())) requestReducer.reduce(&state, .sendThirdRequest(ThirdRequest()))
Conclusão
Não tenha medo de aprender coisas novas e não tenha medo de aprender programação funcional. Eu acho que as melhores práticas estão na encruzilhada da tecnologia. Tente combinar e tirar melhor de diferentes paradigmas de programação.
Se houver alguma tarefa não trivial, faz sentido olhar para ela de um ângulo diferente.