Hallo Habr. Mein Name ist Maxim, ich bin ein iOS-Entwickler bei FINCH. Heute zeige ich Ihnen einige Methoden zur Verwendung der funktionalen Programmierung, die wir in unserer Abteilung entwickelt haben.
Ich möchte sofort darauf hinweisen, dass ich Sie nicht dringend auffordere, überall funktionale Programmierung zu verwenden - dies ist kein Allheilmittel für alle Probleme. In einigen Fällen scheint mir FP jedoch die flexibelsten und elegantesten Lösungen für nicht standardmäßige Probleme bieten zu können.
FP ist ein beliebtes Konzept, daher werde ich die Grundlagen nicht erläutern. Ich bin sicher, dass Sie bereits map, redu, compactMap, first (where :) und ähnliche Technologien in Ihren Projekten verwenden. Der Artikel konzentriert sich auf die Lösung des Problems mehrerer Abfragen und die Arbeit mit dem Reduzierer.
Problem mit mehreren Abfragen
Ich arbeite im Outsourcing der Produktion und es gibt Situationen, in denen sich ein Kunde mit seinen Subunternehmern um die Erstellung eines Backends kümmert. Dies ist alles andere als das bequemste Backend, und Sie müssen mehrere und parallele Abfragen durchführen.
Manchmal könnte ich so etwas schreiben wie:
networkClient.sendRequest(request1) { result in switch result { case .success(let response1):
Ekelhaft, oder? Aber das ist die Realität, mit der ich arbeiten musste.
Ich musste drei aufeinanderfolgende Autorisierungsanfragen senden. Während des Refactorings hielt ich es für eine gute Idee, jede Anforderung in separate Methoden aufzuteilen und sie innerhalb der Fertigstellung aufzurufen, um so eine große Methode zu entladen. Es stellte sich heraus wie:
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) } }
Es ist zu sehen, dass ich in jeder der privaten Methoden Proxy muss
completion: @escaping (Result<AuthResponse>) -> Void
und ich mag es nicht wirklich.
Dann kam mir der Gedanke: "Warum nicht auf funktionale Programmierung zurückgreifen?" Darüber hinaus ermöglicht Swift mit seinem magischen und syntaktischen Zucker, Code auf interessante und verdauliche Weise in einzelne Elemente zu zerlegen.
Zusammensetzung und Reduzierer
Die funktionale Programmierung ist eng mit dem Konzept der Komposition verbunden - Mischen, etwas kombinieren. In der funktionalen Programmierung schlägt die Komposition vor, dass wir das Verhalten einzelner Blöcke kombinieren und dann in Zukunft damit arbeiten.
Die Zusammensetzung aus mathematischer Sicht ist so etwas wie:
func compose<A,B,C>(_ f: @escaping (A) -> B, and g: @escaping (B) -> C) -> (A) -> C { return { a in g(f(a)) } }
Es gibt Funktionen f und g, die intern Ausgabe- und Eingabeparameter angeben. Wir möchten aus diesen Eingabemethoden eine Art resultierendes Verhalten erhalten.
Als Beispiel können Sie zwei Schließungen vornehmen, von denen eine die Eingangszahl um 1 erhöht und die zweite mit sich selbst multipliziert.
let increment: (Int) -> Int = { value in return value + 1 } let multiply: (Int) -> Int = { value in return value * value }
Aus diesem Grund möchten wir beide Operationen anwenden:
let result = compose(multiply, and: increment) result(10)
Leider ist mein Beispiel nicht assoziativ
(Wenn wir inkrementieren und multiplizieren, erhalten wir die Zahl 121), aber lassen wir diesen Moment vorerst weg.
let result = compose(increment, and: multiply) result(10)
PS Ich versuche ausdrücklich, meine Beispiele zu vereinfachen, damit es so klar wie möglich ist.)
In der Praxis müssen Sie häufig Folgendes tun:
let value: Int? = array .lazy .filter { $0 % 2 == 1 } .first(where: { $0 > 10 })
Dies ist die Zusammensetzung. Wir setzen die Eingabeaktion und erhalten einen Ausgabeeffekt. Dies ist jedoch nicht nur das Hinzufügen einiger Objekte - dies ist das Hinzufügen eines ganzen Verhaltens.
Und jetzt lasst uns abstrakter denken :)
In unserer Bewerbung haben wir einen Zustand. Dies kann der Bildschirm sein, den der Benutzer gerade sieht, oder die aktuellen Daten, die in der Anwendung gespeichert sind, usw.
Darüber hinaus haben wir eine Aktion - dies ist die Aktion, die der Benutzer ausführen kann (klicken Sie auf die Schaltfläche, scrollen Sie durch die Sammlung, schließen Sie die Anwendung usw.). Infolgedessen arbeiten wir an diesen beiden Konzepten und verbinden sie miteinander, dh wir kombinieren, hmmm, wir kombinieren (irgendwo habe ich es schon einmal gehört).
Aber was ist, wenn Sie eine Entität erstellen, die nur meinen Zustand und mein Handeln miteinander verbindet?
Also bekommen wir Reducer
struct Reducer<S, A> { let reduce: (S, A) -> S }
Wir werden den aktuellen Status und die Aktion für die Eingabe der Reduktionsmethode angeben, und am Ausgang erhalten wir einen neuen Status, der innerhalb der Reduktion gebildet wurde.
Wir können diese Struktur auf verschiedene Arten beschreiben: indem wir einen neuen Zustand definieren, eine funktionale Methode verwenden oder veränderbare Modelle verwenden.
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 }
Die erste Option ist "klassisch".
Der zweite ist funktionaler. Der Punkt ist, dass wir nicht den Status zurückgeben, sondern eine Methode, die Maßnahmen ergreift, die wiederum bereits den Status zurückgibt. Dies ist im Wesentlichen das Curry der Reduktionsmethode.
Die dritte Option besteht darin, mit dem Status als Referenz zu arbeiten. Bei diesem Ansatz geben wir nicht nur den Status aus, sondern arbeiten mit einem Verweis auf das eingehende Objekt. Es scheint mir, dass diese Methode nicht sehr gut ist, weil solche (veränderlichen) Modelle schlecht sind. Es ist besser, den neuen Status (Instanz) neu zu erstellen und zurückzugeben. Der Einfachheit halber und zur Demonstration weiterer Beispiele stimmen wir jedoch zu, die letztere Option zu verwenden.
Reduzierstück auftragen
Wir wenden das Reducer-Konzept auf den vorhandenen Code an - erstellen Sie einen RequestState, initialisieren Sie ihn und legen Sie ihn fest.
class RequestState {
Zur Synchronisation von Anfragen habe ich DispatchSemaphore hinzugefügt
Mach weiter. Jetzt müssen wir eine RequestAction mit beispielsweise drei Anforderungen erstellen.
enum RequestAction { case sendFirstRequest(FirstRequest) case sendSecondRequest(SecondRequest) case sendThirdRequest(ThirdRequest) }
Erstellen Sie nun einen Reduzierer mit einem RequestState und einer RequestAction. Wir legen das Verhalten fest - was wollen wir mit der ersten, zweiten, dritten Anfrage machen?
let requestReducer = Reducer<RequestState, RequestAction> { state, action in switch action { case .sendFirstRequest(let request): state.sendRequest(request) { (result: Result<FirstResponse>) in
Am Ende nennen wir diese Methoden. Es stellt sich ein deklarativerer Stil heraus, bei dem klar ist, dass die erste, zweite und dritte Anfrage kommen. Alles ist lesbar und klar.
var state = RequestState() requestReducer.reduce(&state, .sendFirstRequest(FirstRequest())) requestReducer.reduce(&state, .sendSecondRequest(SecondRequest())) requestReducer.reduce(&state, .sendThirdRequest(ThirdRequest()))
Fazit
Haben Sie keine Angst, neue Dinge zu lernen, und haben Sie keine Angst, funktionale Programmierung zu lernen. Ich denke, die Best Practices befinden sich am Scheideweg der Technologie. Versuchen Sie, verschiedene Programmierparadigmen zu kombinieren und besser zu nutzen.
Wenn es eine nicht triviale Aufgabe gibt, ist es sinnvoll, sie aus einem anderen Blickwinkel zu betrachten.