Eine kleine Übung der funktionalen Programmierung in Swift für Anfänger



Ich möchte Anfängern das Konzept der funktionalen Programmierung auf einfachste Weise vorstellen und einige seiner Vorteile gegenüber den vielen anderen hervorheben, die den Code wirklich lesbarer und ausdrucksvoller machen. Ich habe einige interessante Demos für Sie auf dem Playground auf Github aufgenommen .

Funktionsprogrammierung: Definition


Erstens ist die funktionale Programmierung keine Sprache oder Syntax, sondern höchstwahrscheinlich eine Möglichkeit, Probleme zu lösen, indem komplexe Prozesse in einfachere und ihre nachfolgende Zusammensetzung zerlegt werden. Wie der Name schon sagt, " Functional Programming ", ist die Kompositionseinheit für diesen Ansatz eine Funktion ; und der Zweck einer solchen Funktion besteht darin, zu vermeiden, dass sich der Zustand oder die Werte außerhalb ihres scope) ändern.

In Swift World gibt es dafür alle Bedingungen, da die Funktionen hier ebenso voll am Programmierprozess beteiligt sind wie Objekte, und das Problem der mutation auf der Ebene des value TYPES (Strukturstrukturen und Enumerationen) gelöst wird, die bei der Verwaltung der Mutabilität ( mutation helfen. ) und klar kommunizieren, wie und wann dies geschehen kann.

Swift nicht im vollen Sinne die Sprache der funktionalen Programmierung , sondern zwingt Sie nicht zur funktionalen Programmierung , obwohl es die Vorteile funktionaler Ansätze erkennt und Wege findet, sie einzubetten.

In diesem Artikel konzentrieren wir uns darauf, die in Swift Elemente der funktionalen Programmierung zu verwenden (dh "out of the box") und zu verstehen, wie Sie sie bequem in Ihrer Anwendung verwenden können.

Imperative und funktionale Ansätze: Vergleich


Um den funktionalen Ansatz zu bewerten, vergleichen wir die Lösungen auf zwei verschiedene Arten mit einem einfachen Problem. Die erste Lösung ist " Imperativ ", bei der der Code den Status innerhalb des Programms ändert.

 //Imperative Approach var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] for i in 0..<numbers.count { let timesTen = numbers[i] * 10 numbers[i] = timesTen } print(numbers) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Beachten Sie, dass wir die Werte innerhalb des veränderlichen Arrays mit dem Namen numbers bearbeiten und dann auf der Konsole drucken. Versuchen Sie anhand dieses Codes, die folgenden Fragen zu beantworten, die wir in naher Zukunft diskutieren werden:

  1. Was versuchen Sie mit Ihrem Code zu erreichen?
  2. Was passiert, wenn ein anderer thread versucht, auf das numbers zuzugreifen, während Ihr Code ausgeführt wird?
  3. Was passiert, wenn Sie Zugriff auf die ursprünglichen Werte im numbers haben möchten?
  4. Wie zuverlässig kann dieser Code getestet werden?

Schauen wir uns nun einen alternativen " funktionalen " Ansatz an:

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] extension Array where Element == Int { func timesTen() -> [Int] { var output = [Int]() for num in self { output.append(num * 10) } return output } } let result = numbers.timesTen() print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

In diesem Code erhalten wir das gleiche Ergebnis auf der Konsole und nähern uns der Lösung des Problems auf völlig andere Weise. Beachten Sie, dass unser numbers diesmal dank des Schlüsselworts let unveränderlich ist. Wir haben den Prozess des Multiplizierens von Zahlen aus dem numbers in die timesTen() -Methode timesTen() , die sich in der extension Array . Wir verwenden immer noch eine for Schleife und ändern eine Variable namens output , aber der scope dieser Variablen wird nur durch diese Methode begrenzt. In ähnlicher Weise wird unser Eingabeargument self als Wert ( by value ) an die timesTen() -Methode übergeben und hat denselben Gültigkeitsbereich wie die Ausgabe der output . Die timesTen() -Methode wird aufgerufen, und wir können sowohl das ursprüngliche numbers als auch das Ergebnisarray- result auf der Konsole drucken.
Kommen wir zurück zu unseren 4 Fragen.

1. Was versuchst du mit deinem Code zu erreichen?

In unserem Beispiel führen wir eine sehr einfache Aufgabe aus, indem wir die Zahlen im numbers mit 10 multiplizieren.

Bei einem imperativen Ansatz müssen Sie, um eine Ausgabe zu erhalten, wie ein Computer denken und den Anweisungen in der for Schleife folgen. In diesem Fall zeigt der Code, wie Sie das Ergebnis erzielen. Beim funktionalen Ansatz wird " " in die timesTen() -Methode "eingeschlossen". Vorausgesetzt, diese Methode wurde an anderer Stelle implementiert, können Sie wirklich nur den Ausdruck numbers.timesTen() . Ein solcher Code zeigt deutlich, durch diesen Code erreicht wird und nicht, wie die Aufgabe gelöst wird. Dies wird als deklarative Programmierung bezeichnet , und es ist leicht zu erraten, warum ein solcher Ansatz attraktiv ist. Der zwingende Ansatz lässt den Entwickler verstehen, Code funktioniert, um zu bestimmen, er tun soll. Der funktionale Ansatz im Vergleich zum imperativen Ansatz ist viel „ausdrucksvoller“ und bietet dem Entwickler die luxuriöse Möglichkeit, einfach anzunehmen, dass die Methode das tut, was sie behauptet zu tun! (Diese Annahme gilt natürlich nur für vorverifizierten Code).

2. Was passiert, wenn ein anderer thread versucht, auf das numbers zuzugreifen, während Ihr Code ausgeführt wird?

Die oben dargestellten Beispiele existieren in einem vollständig isolierten Raum, obwohl es in einer komplexen Umgebung mit mehreren Threads durchaus möglich ist, dass zwei threads versuchen, auf dieselben Ressourcen zuzugreifen. Im Fall des imperativen Ansatzes ist leicht zu erkennen, dass das Ergebnis durch die Reihenfolge bestimmt wird, in der die threads auf das numbers zugreifen, wenn ein anderer thread während der Verwendung Zugriff auf das numbers . Diese Situation wird als race condition und kann zu unvorhersehbarem Verhalten und sogar zu Instabilität und Absturz der Anwendung führen.

Im Vergleich dazu hat der funktionale Ansatz keine „Nebenwirkungen“. Mit anderen Worten, die Ausgabe der output ändert keine in unserem System gespeicherten Werte und wird ausschließlich durch die Eingabe bestimmt. In diesem Fall erhält jeder Thread ( threads ), der Zugriff auf das numbers , IMMER dieselben Werte und sein Verhalten ist stabil und vorhersehbar.

3. Was passiert, wenn Sie Zugriff auf die im numbers gespeicherten Originalwerte haben möchten?

Dies ist eine Fortsetzung unserer Diskussion über „Nebenwirkungen“. Offensichtlich werden Statusänderungen nicht verfolgt. Daher verlieren wir mit dem imperativen Ansatz den Anfangszustand unseres numbers während des Konvertierungsprozesses. Unsere auf dem funktionalen Ansatz basierende Lösung speichert das ursprüngliche numbers und generiert ein neues result mit den gewünschten Eigenschaften am Ausgang. Das ursprüngliche numbers bleibt erhalten und ist für die zukünftige Verarbeitung geeignet.

4. Wie zuverlässig kann dieser Code getestet werden?

Da der funktionale Ansatz alle „Nebenwirkungen“ zerstört, liegt die getestete Funktionalität vollständig innerhalb der Methode. Bei der Eingabe dieser Methode werden NIEMALS Änderungen vorgenommen, sodass Sie den Zyklus mehrmals testen können, so oft Sie möchten, und IMMER das gleiche Ergebnis erhalten. In diesem Fall ist das Testen sehr einfach. Im Vergleich dazu ändert das Testen der Imperative- Lösung in einer Schleife den Beginn des Eintrags und Sie erhalten nach jeder Iteration völlig andere Ergebnisse.

Zusammenfassung der Vorteile


Wie wir anhand eines sehr einfachen Beispiels gesehen haben, ist der funktionale Ansatz eine coole Sache, wenn Sie sich mit einem Datenmodell befassen, weil:

  • Es ist deklarativ
  • Es behebt Thread-bezogene Probleme wie race condition und Deadlocks
  • Der Zustand bleibt unverändert, der für nachfolgende Transformationen verwendet werden kann.
  • Es ist leicht zu testen.

Lassen Sie uns etwas weiter gehen, um funktionale Programmierung in Swift lernen. Es wird davon ausgegangen, dass die Hauptakteure Funktionen sind und in erster Linie Objekte der ersten Klasse sein sollten .

First Class-Funktionen und Funktionen höherer Ordnung


Damit eine Funktion erstklassig ist, muss sie als Variable deklariert werden können. Auf diese Weise können Sie die Funktion als normalen Datentyp verwalten und gleichzeitig ausführen. Glücklicherweise sind Funktionen in Swift Objekte der ersten Klasse, dh sie werden unterstützt, indem sie als Argumente an andere Funktionen übergeben, als Ergebnis anderer Funktionen zurückgegeben, Variablen zugewiesen oder in Datenstrukturen gespeichert werden.

Aus diesem Grund haben wir in Swift andere Funktionen - Funktionen höherer Ordnung, die als Funktionen definiert sind, die eine andere Funktion als Argument verwenden oder eine Funktion zurückgeben. Es gibt viele davon: map , filter , flatMap , compactMap , flatMap , compactMap , compactMap usw. Die häufigsten Beispiele für Funktionen höherer Ordnung sind map , filter und reduce . Sie sind nicht global, sie sind alle an bestimmte Typen gebunden. Sie arbeiten mit allen Sequenztypen, einschließlich der Collection , die durch Swift Datenstrukturen wie ein Array , ein Dictionary und einen Set . In Swift 5 funktionieren Funktionen höherer Ordnung auch mit einem völlig neuen TYPE - Result .

map(_:)


In Swift map(_:) eine Funktion als Parameter und konvertiert die Werte eines bestimmten gemäß dieser Funktion. Wenn Sie beispielsweise map(_:) auf ein Array von Array Werten anwenden, wenden wir eine Parameterfunktion auf jedes Element des ursprünglichen Arrays an und erhalten ein Array von Array , aber auch die konvertierten Werte.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] func timesTen(_ x:Int) -> Int { return x * 10 } let result = numbers.map (timesTen) print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Im obigen Code haben wir die Funktion timesTen (_:Int) , die einen ganzzahligen Int Wert annimmt und den ganzzahligen Wert Int multipliziert mit 10 zurückgibt, und ihn als Eingabeparameter für unsere map(_:) Funktion höherer Ordnung map(_:) verwendet und auf unser Array angewendet numbers . Wir haben das Ergebnis, das wir brauchen, im result .

Der Name der Parameterfunktion timesTen für Funktionen höherer Ordnung wie map(_:) spielt keine Rolle, der Eingabeparameters und der Rückgabewert sind wichtig, timesTen die Signatur (Int) -> Int Funktionseingabeparameters. Daher können wir anonyme Funktionen in map(_:) - Schließungen - in jeder Form verwenden, einschließlich solcher mit verkürzten Argumentnamen $0 , $1 usw.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let result = numbers.map { $0 * 10 } print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Wenn wir uns die Funktion map(_ :) für ein Array ansehen, könnte dies folgendermaßen aussehen:

 func map<T>(_ transform: (Element) -> T) -> [T] { var returnValue = [T]() for item in self { returnValue.append(transform(item)) } return returnValue } 

Dies ist ein zwingender Code, der uns vertraut ist, aber es ist kein Entwicklerproblem mehr, es ist ein Apple Problem, ein Swift Problem. Die Implementierung der map(_:) -Funktion höherer Ordnung wird von Apple hinsichtlich der Leistung optimiert, und uns Entwicklern wird die map(_:) -Funktionalität garantiert, sodass wir nur mit der transform wir wollen, korrekt ausdrücken können, ohne uns Sorgen machen zu müssen es umgesetzt wird. Als Ergebnis erhalten wir perfekt lesbaren Code in Form einer einzelnen Zeile, der besser und schneller funktioniert.

 //Functional Approach let possibleNumbers = ["1", "2", "three", "///4///", "5"] let mapped = possibleNumbers.map {str in Int(str) } print (mapped) // [Optional(1), Optional(2), nil, nil, Optional(5)] 

Der von der Parameterfunktion zurückgegebene stimmt möglicherweise nicht mit dem Elemente in der ursprünglichen Sammlung überein.

Im obigen Code haben wir mögliche Ganzzahlen mit möglichen Zahlen, die als Zeichenfolgen dargestellt werden, und wir möchten sie in Ganzzahlen von Int konvertieren, indem failable den failable Initialisierer Int(_ :String) failable Int(_ :String) der durch den Abschluss { str in Int(str) } . Wir tun dies mit map(_:) und erhalten ein mapped Array von Optional als Ausgabe:



Wir konnten Elemente unseres Arrays mit possibleNumbers Zahlen in Ganzzahlen konvertieren. Daher erhielt ein Teil den Wert nil , was darauf hinweist, dass es unmöglich ist, die String in eine Ganzzahl Int konvertieren, und der andere Teil wurde in Optionals , die Werte haben:

 print (mapped) // [Optional(1), Optional(2), nil, nil, Optional(5)] 

compactMap(_ :)


Wenn die an die Funktion höherer Ordnung übergebene Parameterfunktion am Ausgang einen Optional Wert hat, kann es sinnvoller sein, eine andere Funktion höherer Ordnung zu verwenden, deren Bedeutung ähnlich ist - compactMap(_ :) , die dasselbe tut wie map(_:) , erweitert aber zusätzlich die am Optional Ausgang empfangenen Werte und entfernt nil Werte aus der Sammlung.



In diesem Fall erhalten wir ein Array von compactMapped TYPE [Int] , aber möglicherweise kleiner:

 let possibleNumbers = ["1", "2", "three", "///4///", "5"] let compactMapped = possibleNumbers.compactMap(Int.init) print (compactMapped) // [1, 2, 5] 



Wann immer Sie den Initializer init?() Als Transformationsfunktion verwenden, müssen Sie compactMap(_ :) :

 // Validate URLs let strings = ["https://demo0989623.mockable.io/car/1", "https://i.imgur.com/Wm1xcNZ.jpg"] let validateURLs = strings.compactMap(URL.init) // Separate Numbers and Operations let mathString: String = "12-37*2/5+44" let numbers1 = mathString.components(separatedBy: ["-", "*", "+", "/"]).compactMap(Int.init) print(numbers1) // [12, 37, 2, 5, 44] 

Ich muss sagen, dass es mehr als genug Gründe gibt, die Funktion compactMap(_ :) höherer Ordnung compactMap(_ :) . Swift "liebt" Optional Werte, die nicht nur mit dem Initializer " failable " init?() , failable auch mit dem as? "Casting" :

 let views = [innerView,shadowView,logoView] let imageViews = views.compactMap{$0 as? UIImageView} 

... und die try? bei der Verarbeitung von Fehlern, die von einigen Methoden ausgelöst werden. Ich muss sagen, dass Apple besorgt ist, dass die Verwendung von try? führt sehr oft zu doppeltem Optional und lässt in Swift 5 nur noch ein Optional Level nach dem Anwenden von try? .

Es gibt eine weitere ähnliche Funktion im Namen der flatMap(_ :) höherer Ordnung flatMap(_ :) , über die etwas niedriger ist.

Um die Funktionszuordnung höherer Ordnung map(_:) , ist es manchmal nützlich, die Methode zip (_:, _:) verwenden, um eine Folge von Paaren aus zwei verschiedenen Folgen zu erstellen.

Angenommen, wir haben eine view in der mehrere Punkte dargestellt werden, die miteinander verbunden sind und eine gestrichelte Linie bilden:



Wir müssen eine weitere unterbrochene Linie erstellen, die die Mittelpunkte der Segmente der ursprünglichen unterbrochenen Linie verbindet:



Um den Mittelpunkt eines Segments zu berechnen, müssen wir die Koordinaten von zwei Punkten haben: dem aktuellen und dem nächsten. Dazu können wir mit der zip (_:, _:) Methode zip (_:, _:) eine Sequenz aus Punktpaaren - dem aktuellen und dem nächsten - bilden, in der wir das Startpunktarray und ein Array der folgenden points.dropFirst() :

 let pairs = zip (points,points.dropFirst()) let averagePoints = pairs.map { CGPoint(x: ($0.x + $1.x) / 2, y: ($0.y + $1.y) / 2 )} 

Mit einer solchen Sequenz berechnen wir die Mittelpunkte sehr einfach mithilfe der Funktionskarte höherer Ordnung map(_:) und zeigen sie in der Grafik an.

filter (_:)


In Swift ist der Funktionsfilter höherer Ordnung filter (_:) die meisten verfügbar, die die map(_:) Funktion map(_:) verfügbar ist. Sie können beliebige Sequence mit filter (_:) , das liegt auf der Hand! Die Methode filter (_:) verwendet eine andere Funktion als Parameter, die eine Bedingung für jedes Element der Sequenz darstellt. Wenn die Bedingung erfüllt ist, wird das Element in das Ergebnis aufgenommen, und wenn nicht, wird es nicht berücksichtigt. Diese "andere Funktion" nimmt einen einzelnen Wert an - ein Element der Sequence - und gibt einen Bool , das sogenannte Prädikat.

Bei Array Arrays wendet beispielsweise der Funktionsfilter höherer Ordnung filter (_:) die Prädikatfunktion an und gibt ein anderes Array zurück, das ausschließlich aus den Elementen des ursprünglichen Arrays besteht, für die die Eingabe-Prädikatfunktion true zurückgibt.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let filted = numbers.filter{$0 % 2 == 0} //[2, 4, 6, 8, 10] 

Hier nimmt der Funktionsfilter höherer Ordnung filter (_:) jedes Element des numbers (dargestellt durch $0 ) und prüft, ob dieses Element eine gerade Zahl ist. Wenn dies eine gerade Zahl ist, fallen die Elemente des numbers in das neue filted Array, andernfalls nicht. Wir haben dem Programm in einer deklarativen Form mitgeteilt, wir bekommen möchten, anstatt uns darum zu kümmern, wie wir es tun sollen.

Ich werde ein weiteres Beispiel für die Verwendung des Funktionsfilters höherer Ordnung filter (_:) , um nur die geraden ersten 20 Fibonacci-Zahlen mit Werten < 4000 :

 let fibonacci = sequence(first: (0, 1), next: { ($1, $0 + $1) }) .prefix(20).map{$0.0} .filter {$0 % 2 == 0 && $0 < 4000} print (fibonacci) // [0, 2, 8, 34, 144, 610, 2584] 

Wir erhalten eine Folge von Tupeln, die aus zwei Elementen der Fibonacci-Folge besteht: dem n-ten und (n + 1) -ten:

 (0, 1), (1, 1), (1, 2), (2, 3), (3, 5) … 

Zur weiteren Verarbeitung begrenzen wir die Anzahl der Elemente mit dem prefix (20) auf die einundzwanzigsten Elemente und nehmen das 0 Element des gebildeten Tupels mit map {$0.0 } , das der Fibonacci-Sequenz ab 0 :

 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,... 

Wir könnten das 1 Element des gebildeten Tupels mit der map {$0.1 } , die der Fibonacci-Sequenz entspricht, die mit 1 beginnt:

 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,... 

Wir erhalten die benötigten Elemente mithilfe des Funktionsfilters höherer Ordnung filter {$0 % 2 == 0 && $0 < 4000} , der ein Array von Sequenzelementen zurückgibt, die das angegebene Prädikat erfüllen. In unserem Fall handelt es sich um ein Array von Ganzzahlen [Int] :

 [0, 2, 8, 34, 144, 610, 2584] 

Es gibt ein weiteres nützliches Beispiel für die Verwendung von filter (_:) für eine Collection .

Ich bin auf ein echtes Problem gestoßen, wenn Sie eine Reihe von Bildern haben, die mit CollectionView angezeigt werden, und Sie können die Drag & Drop Technologie verwenden, um ein ganzes „Paket“ von Bildern zu sammeln und sie überall hin zu verschieben, einschließlich „Dumping“ auf „ Mülltonne".



In diesem Fall ist das Array der entfernten Indizes removedIndexes die in den "Papierkorb" removedIndexes behoben, und Sie müssen ein neues Array von Bildern erstellen, mit Ausnahme derjenigen, deren Indizes sich im Array " removedIndexes . Angenommen, wir haben ein Array von Ganzzahlbildern, die Bilder imitieren, und ein Array von Indizes dieser Ganzzahlen removedIndexes , die entfernt werden müssen. Wir werden filter (_:) , um unser Problem zu lösen:

 var images = [6, 22, 8, 14, 16, 0, 7, 9] var removedIndexes = [2,5,0,6] var images1 = images .enumerated() .filter { !removedIndexes.contains($0.offset) } .map { $0.element } print (images1) // [22, 14, 16, 9] 

Die Methode enumerated() gibt eine Folge von Tupeln zurück, die aus Versatzindizes und Arrayelementwerten besteht.Dann wenden wir einen Filter filterauf die resultierende Folge von Tupeln an und lassen nur diejenigen übrig, deren Index $0.offsetnicht im Array enthalten ist removedIndexes. Im nächsten Schritt wählen wir den Wert aus dem Tupel aus $0.elementund erhalten das Array, das wir benötigen images1.

reduce (_:, _:)


Die Methode steht reduce (_:, _:)auch den meisten verfügbaren Methoden map(_:)und Methoden zur Verfügung filter (_:). Die Methode reduce (_:, _:)"reduziert" die Sequenz Sequenceauf einen einzelnen Akkumulationswert und hat zwei Parameter. Der erste Parameter ist der Startakkumulationswert, und der zweite Parameter ist eine Funktion, die den Akkumulationswert mit dem Sequenzelement kombiniert Sequence, um einen neuen Akkumulationswert zu erhalten.

Die Eingabeparameterfunktion wird nacheinander auf jedes Element der Sequenz angewendet Sequence, bis es das Ende erreicht und den endgültigen Akkumulationswert erzeugt.

 let sum = Array (1...100).reduce(0, +) 

Dies ist ein klassisches triviales Beispiel für die Verwendung einer Funktion höherer Ordnung reduce (_:, _:)- das Zählen der Summe der Elemente eines Arrays Array.

     1 0 1 0 +1 = 1 2 1 2 2 + 1 = 3 3 3 3 3 + 3 = 6 4 6 4 4 + 6 = 10 . . . . . . . . . . . . . . . . . . . 100 4950 100 4950 + 100 = 5050 

Mit der Funktion können reduce (_:, _:)wir ganz einfach die Summe der Fibonacci-Zahlen berechnen, die eine bestimmte Bedingung erfüllen:

 let fibonacci = sequence(first: (0, 1), next: { ($1, $0 + $1) }) .prefix(20).map{$0.0} .filter {$0 % 2 == 0 && $0 < 4000} print (fibonacci) // [0, 2, 8, 34, 144, 610, 2584] print(fibonacci.reduce(0,+)) // 3382 

Es gibt jedoch interessantere Anwendungen einer Funktion höherer Ordnung reduce (_:, _:).

Zum Beispiel können wir sehr einfach und präzise einen sehr wichtigen Parameter für UIScrollView- die Größe des "scrollbaren" Bereichs contentSize- basierend auf seiner Größe bestimmen subviews:

 let scrollView = UIScrollView() scrollView.addSubview(UIView(frame: CGRect(x: 300.0, y: 0.0, width: 200, height: 300))) scrollView.addSubview(UIView(frame: CGRect(x: 100.0, y: 0.0, width: 300, height: 600))) scrollView.contentSize = scrollView.subviews .reduce(CGRect.zero,{$0.union($1.frame)}) .size // (500.0, 600.0) 

In dieser Demo ist der Akkumulationswert GCRectund die Akkumulationsoperation ist die Operation zum Kombinieren der unionRechtecke, die frameuns gehören subviews.

Trotz der Tatsache, dass eine Funktion höherer Ordnung reduce (_:, _:)einen akkumulativen Charakter annimmt, kann sie in einer völlig anderen Perspektive verwendet werden. So teilen Sie beispielsweise ein Tupel in Teile einer Reihe von Tupeln auf:

 // Separate Tuples let arr = [("one", 1), ("two", 2), ("three", 3), ("four", 4)] let (arr1, arr2) = arr.reduce(([], [])) { ($0.0 + [$1.0], $0.1 + [$1.1]) } print(arr1) // ["one", "two", "three", "four"] print(arr2) // [1, 2, 3, 4] 

Swift 4.2führte eine neue Art von Funktion höherer Ordnung ein reduce (into:, _:). Das Verfahren reduce (into:, _:)ist dadurch vorteilhaft, die Effizienz im Vergleich mit dem Verfahren reduce (:, :), wenn die sich ergebende Struktur verwendet wird COW (copy-on-write) , beispielsweise, Arrayoder Dictionary.

Es kann effektiv verwendet werden, um übereinstimmende Werte in einem Array von Ganzzahlen zu entfernen:

 // Remove duplicates let arrayInt = [1,1,2,6,6,7,2,9,7].reduce(into: []) { !$0.contains($1) ? $0.append($1) : () } // [1, 2, 6, 7, 9] 

... oder beim Zählen der Anzahl verschiedener Elemente in einem Array:

 // Count equal elements in array let arrayIntCount = [1,1,2,2,6,6,7,2,9,7].reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1 } // [6: 2, 9: 1, 1: 2, 2: 3, 7: 2] 

flatMap (_:)


Bevor wir zu dieser Funktion höherer Ordnung übergehen, schauen wir uns eine sehr einfache Demo an.

 let maybeNumbers = ["42", "7", "three", "///4///", "5"] let firstNumber = maybeNumbers.map (Int.init).first 

Wenn wir diesen Code ausführen Playground, um ihn auszuführen , sieht alles gut aus, und unser Code firstNumberist gleich 42:



Wenn Sie es nicht wissen, Playgroundverbirgt er häufig den wahren Code , insbesondere Konstanten firstNumber. In der Tat, die konstant firstNumberist doppelt Optional:



Dies tritt auf, weil map (Int.init)das Ausgangsarray erzeugt OptionalTypwerte [Int?]als jede Zeile nicht ist , Stringzu transformieren kann Intund Initialisierer Int.intwird „fallenden“ ( failable). Dann nehmen wir das erste Element des gebildeten Arrays mit der Funktion firstfür das Array Array, die auch die Ausgabe bildetOptional, da das Array möglicherweise leer ist und wir das erste Element des Arrays nicht erhalten können. Als Ergebnis haben wir ein Doppel Optional, d.h.Int?? .

Wir haben eine verschachtelte Struktur, Optionalin Optionalder es wirklich schwieriger ist zu arbeiten und die wir natürlich nicht haben wollen. Um den Wert dieser verschachtelten Struktur zu erhalten, müssen wir in zwei Ebenen „eintauchen“. Darüber hinaus können zusätzliche Transformationen den Pegel Optionalnoch weiter vertiefen .

Es ist Optionalwirklich mühsam, den Wert aus dem doppelt verschachtelten herauszuholen .

Wir haben 3 Optionen und alle erfordern fundierte Sprachkenntnisse Swift.

  • if let , ; «» «» Optional , — «» Optional :

  • if case let ( pattern match ) :



    ?? :

  • , switch :


Schlimmer noch, solche Verschachtelungsprobleme treten in allen Situationen auf, in denen generalisierte ( generic) Container enthalten sind, für die eine Operation definiert ist map. Zum Beispiel für Arrays Array.

Betrachten Sie einen anderen Beispielcode. Angenommen, wir haben einen mehrzeiligen Text multilineString, den wir in Wörter in Kleinbuchstaben (Kleinbuchstaben) unterteilen möchten:

 let multilineString = """  ,  ,   ;     , —  ,   :  —   ,   .   ,   ,   .    .  ,        ,  « »  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .map{$0.split(separator: " ")} 

Um ein Array von Wörtern zu erhalten words, machen wir zuerst Großbuchstaben (groß) mit der Methode in Kleinbuchstaben (klein) lowercased(). Dann teilen wir den Text mit der Methode in split(separatot: "\n")Zeilen und erhalten ein Array von Zeichenfolgen. Anschließend map {$0.split(separator: " ")}trennen wir jede Zeile in separate Wörter.

Als Ergebnis erhalten wir verschachtelte Arrays:

 [["", ",", "", ","], ["", "", ";", "", "", "", "", ",", "—"], ["", ",", "", "", ":"], ["", "—", "", "", ",", "", "", "."], ["", "", ",", "", "", ","], ["", "", ".", "", ""], ["", ".", "", ",", ""], ["", "", "", ""], ["", "", ",", "", "«", "»"], ["", ".", "", ","], ["", ",", "", "", "!"]] 

... und es wordshat zwei Dinge Array:



Wir haben wieder eine "verschachtelte" Datenstruktur, aber diesmal haben wir es nicht getan Optional, aber Array. Wenn wir die empfangenen Wörter weiter verarbeiten möchten words, um beispielsweise das Buchstabenspektrum dieses mehrzeiligen Textes zu finden, müssen wir zuerst das Array des Doppelten irgendwie „begradigen“ Arrayund es in ein einzelnes Array verwandeln Array. Dies ähnelt dem, was wir mit double Optionalfür eine Demo am Anfang dieses Abschnitts gemacht haben flatMap:

 let maybeNumbers = ["42", "7", "three", "///4///", "5"] let firstNumber = maybeNumbers.map (Int.init).first 

Glücklicherweise müssen Swiftwir nicht auf komplexe syntaktische Konstruktionen zurückgreifen. Swiftliefert uns eine fertige Lösung für Arrays Arrayund Optional. Dies ist eine Funktion höherer Ordnung flatMap! Es ist sehr ähnlich, mapverfügt jedoch über zusätzliche Funktionen, die mit dem anschließenden "Begradigen" der "Anhänge" verbunden sind, die während der Ausführung angezeigt werden map. Und deshalb heißt es flatMap, es „begradigt“ ( flattens) das Ergebnis map.

Lassen Sie sich bewerben flatMapzu firstNumber:



Wir haben wirklich den Ausgangswert von c einziger Ebene bekommen Optional. Funktioniert für ein Array

noch interessanter . In unseren Bedingungen, denn wir einfach ersetzen aufflatMapArraywordsmapflatMap:



... und wir bekommen nur eine Reihe von Wörtern wordsohne "Verschachtelung":

 ["", ",", "", ",", "", "", ";", "", "", "", "", ",", "—", "", ",", "", "", ":", "", "—", "", "", ",", "", "", ".", "", "", ",", "", "", ",", "", "", ".", "", "", "", ".", "", ",", "", "", "", "", "", "", "", ",", "", "«", "»", "", ".", "", ",", "", ",", "", "", "!"] 

Jetzt können wir die Verarbeitung der resultierenden Wortreihe fortsetzen words, aber seien Sie vorsichtig. Wenn wir es erneut flatMapauf jedes Element des Arrays anwenden words, erhalten wir möglicherweise ein unerwartetes, aber durchaus verständliches Ergebnis.



Wir erhalten eine einzelne, nicht "verschachtelte" Anordnung von Buchstaben und Symbolen, [Character]die in unserer mehrzeiligen Phrase enthalten sind:

 ["", "", "", "", "", "", "", "", "", "", "", "", ",", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ",", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ";", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ...] 

Tatsache ist, dass die Zeichenfolge Stringeine Sammlung von CollectionZeichen [Character]ist. Wenn flatMapwir auf jedes einzelne Wort zutreffen, senken wir erneut die Ebene der "Verschachtelung" und kommen zu einer Reihe von Zeichen flattenCharacters.
Vielleicht ist es genau das, was Sie wollen, oder vielleicht auch nicht. Achten Sie darauf.

Alles zusammenfügen: einige Probleme lösen


AUFGABE 1


Wir können die Verarbeitung des im vorherigen Abschnitt erhaltenen Wortarrays fortsetzen wordsund die Häufigkeit des Auftretens von Buchstaben in unserer mehrzeiligen Phrase berechnen. Lassen Sie uns zunächst alle Wörter aus dem Array wordsin eine große Zeile „kleben“ und alle Satzzeichen davon ausschließen, dh nur die Buchstaben belassen:

 let wordsString = words.reduce ("",+).filter { "" .contains($0)} //  

Also haben wir alle Briefe, die wir brauchen. Lassen Sie uns nun ein Wörterbuch daraus erstellen, in dem der Schlüssel keyder Buchstabe und der Wert valuedie Häufigkeit seines Auftretens im Text ist.

Wir können dies auf zwei Arten tun.
Die erste Methode ist mit der Verwendung einer neuen Swift 4.2Variante einer Funktion höherer Ordnung verbunden , die in erschienen ist reduce (into:, _:). Diese Methode eignet sich gut für die Organisation eines Wörterbuchs letterCountmit der Häufigkeit des Auftretens von Buchstaben in unserer mehrzeiligen Phrase:

 let letterCount = wordsString.reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1} print (letterCount) // ["": 1, "": 18, "": 2, "": 2, "": 5, "": 7, "": 17, "": 4, "": 23, ...] 

Als Ergebnis erhalten wir ein Wörterbuch, letterCount [Character : Int]in dem die Schlüssel keydie Zeichen sind, die in der untersuchten Phrase gefunden werden, und als Wert valuedie Anzahl dieser Zeichen.

Die zweite Methode umfasst das Initialisieren des Wörterbuchs mithilfe der Gruppierung, wodurch das gleiche Ergebnis erzielt wird:

 let letterCountDictionary = Dictionary(grouping: wordsString ){ $0}.mapValues {$0.count} letterCount == letterCountDictionary // true 

Wir möchten das Wörterbuch letterCountalphabetisch sortieren :

 let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat) // [":17", ":5", ":18", ":4", ":8", ":35", ":3", ":4", ":18", ":5", ":2", ":10", ":4", ":26", ":34", ":5", ":7", ":23", ":25", ":4", ":2", ":3", ":4", ":2", ":1", ":14", ":2", ":4"] 

Wir können das Wörterbuch jedoch nicht direkt sortieren Dictionary, da es sich grundsätzlich nicht um eine geordnete Datenstruktur handelt. Wenn wir die Funktion sorted (by:)auf das Wörterbuch anwenden Dictionary, werden die Elemente der Sequenz, die mit dem angegebenen Prädikat sortiert sind, in Form eines Arrays benannter Tupel an mapuns zurückgegeben , die wir in ein Array von Zeichenfolgen [":17", ":5", ":18", ...]umwandeln, die die Häufigkeit des Auftretens des entsprechenden Buchstabens widerspiegeln.

Wir sehen, dass diesmal sorted (by:)nur der Operator " <" als Prädikat an eine Funktion höherer Ordnung übergeben wird . Die Funktion sorted (by:)erwartet eine „Vergleichsfunktion“ als einziges Argument am Eingang. Es wird verwendet, um zwei benachbarte Werte zu vergleichen und zu entscheiden, ob sie korrekt sortiert sind (in diesem Fall wird zurückgegebentrue) oder nicht (kehrt zurück false). Wir können dieser "Vergleichsfunktion" Funktionen sorted (by:)in Form eines anonymen Abschlusses geben:

 sorted(by: {$0.key < $1.key} 

Und wir können ihm einfach den Operator " <" geben, der die Signatur hat, die wir brauchen, wie oben beschrieben. Dies ist auch eine Funktion, und die Sortierung nach Schlüsseln wird ausgeführt key.

Wenn wir das Wörterbuch nach Werten sortieren valueund herausfinden möchten , welche Buchstaben in dieser Phrase am häufigsten vorkommen, müssen wir den Abschluss für die Funktion verwenden sorted (by:):

 let countsStat = letterCountDictionary .sorted(by: {$0.value > $1.value}) .map{"\($0.0):\($0.1)"} print (countsStat ) //[":35", ":34", ":26", ":25", ":23", ":18", ":18", ":17", ":14", ":10", ":8", ":7", ":5", ":5", ":5", ":4", ":4", ":4", ":4", ":4", ":4", ":3", ":3", ":2", ":2", ":2", ":2", ":1"] 

Wenn wir uns die Lösung für das Problem der Bestimmung des Buchstabenspektrums einer mehrzeiligen Phrase als Ganzes ansehen ...

 let multilineString = """  ,  ,   ;     , —  ,   :  —   ,   .   ,   ,   .    .  ,        ,  « »  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .flatMap{$0.split(separator: " ")} let wordsString = words.reduce ("",+).filter { "" .contains($0)} let letterCount = wordsString.reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1} let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat) // [":17", ":5", ":18", ":4", ":8", ":35", ":3", ":4", ":18", ":5", ":2", ":10", ":4", ":26", ":34", ":5", ":7", ":23", ":25", ":4", ":2", ":3", ":4", ":2", ":1", ":14", ":2", ":4"] let countsStat = letterCountDictionary .sorted(by: {$0.value > $1.value}) .map{"\($0.0):\($0.1)"} print (countsStat ) //[":35", ":34", ":26", ":25", ":23", ":18", ":18", ":17", ":14", ":10", ":8", ":7", ":5", ":5", ":5", ":4", ":4", ":4", ":4", ":4", ":4", ":3", ":3", ":2", ":2", ":2", ":2", ":1"] 

... dann werden wir feststellen, dass es in diesem Codefragment grundsätzlich keine Variablen gibt (nein var, nur let)alle Namen der verwendeten Funktionen spiegeln AKTIONEN (Funktionen) über bestimmte Informationen wider, ohne sich Gedanken darüber zu machen, wie diese Aktionen implementiert werden:

split- split,
map- transform
flatMap- transform with Ausrichtung (um eine Stufe der Verschachtelung zu entfernen),
filter- Filter,
sorted- Vereinzeln,
reduce- Daten in eine bestimmte Struktur zu drehen mittels einer spezifischen Operation

in diesem Fragmente jeder Codezeile erklärt den Namen der Funktion , die wir verwenden , wenn wir sind. „Reine“ füllt die Transformation verwendet wird , mapwenn wir Umwandlung der Verschachtelungsebene auszuführen verwendet wirdflatMapWenn wir nur bestimmte Daten auswählen möchten, verwenden wir filterusw. Alle diese Funktionen "höchster Ordnung" werden Appleunter Berücksichtigung der Leistungsoptimierung entworfen und getestet . Dieser Code ist also sehr zuverlässig und prägnant - wir brauchten nicht mehr als 5 Sätze, um unser Problem zu lösen. Dies ist ein Beispiel für eine funktionale Programmierung.

Der einzige Nachteil bei der Anwendung des funktionalen Ansatzes in dieser Demo besteht darin, dass wir unseren Text aus Gründen der Unveränderlichkeit, Testbarkeit und Lesbarkeit wiederholt durch verschiedene Funktionen höherer Ordnung verfolgen. Bei einer großen Anzahl von Sammlungsgegenständen kann die CollectionLeistung sinken. Zum Beispiel, wenn wir zuerst filter(_:)und und dann - verwenden first.
InSwift 4 Einige neue Funktionsoptionen wurden hinzugefügt, um die Leistung zu verbessern. Hier finden Sie einige Tipps zum Schreiben von schnellerem Code.

1. containsNICHT verwendenfirst( where: ) != nil


Das Überprüfen, ob sich ein Objekt in einer Sammlung befindet, Collectionkann auf verschiedene Arten erfolgen. Die beste Leistung liefert die Funktion contains.

RICHTIGER CODE

 let numbers = [0, 1, 2, 3] numbers.contains(1) 

Falscher Code

 let numbers = [0, 1, 2, 3] numbers.filter { number in number == 1 }.isEmpty == false numbers.first(where: { number in number == 1 }) != nil 

2. Verwenden Sie die Validierung isEmpty, NICHT einen Vergleich countmit Null


Da für einige Sammlungen der Zugriff auf die Eigenschaft countdurch Iteration über alle Elemente der Sammlung erfolgt.

RICHTIGER CODE

 let numbers = [] numbers.isEmpty 

Falscher Code

 let numbers = [] numbers.count == 0 

3. Überprüfen Sie die leere Zeichenfolge StringmitisEmpty


String Stringin Swiftist eine Sammlung von Zeichen [Character]. Dies bedeutet, dass es für Strings Stringauch besser ist, diese zu verwenden isEmpty.

RICHTIGER CODE

 myString.isEmpty 

Falscher Code

 myString == "" myString.count == 0 

4. Erhalten des ersten Elements, das bestimmte Bedingungen erfüllt


Das Durchlaufen der gesamten Sammlung, um das erste Objekt zu erhalten, das bestimmte Bedingungen erfüllt, kann mit einer Methode filtergefolgt von einer Methode durchgeführt firstwerden. Die Methode ist jedoch hinsichtlich der Geschwindigkeit die beste first (where:). Diese Methode beendet die Iteration über die Sammlung, sobald sie die erforderliche Bedingung erfüllt. Die Methode filteriteriert weiterhin über die gesamte Sammlung, unabhängig davon, ob sie die erforderlichen Elemente erfüllt oder nicht.

Dies gilt natürlich auch für die Methode last (where:).

RICHTIGER CODE

 let numbers = [3, 7, 4, -2, 9, -6, 10, 1] let firstNegative = numbers.first(where: { $0 < 0 }) 

Falscher Code

 let numbers = [0, 2, 4, 6] let allEven = numbers.filter { $0 % 2 != 0 }.isEmpty 

Manchmal, wenn die Sammlung Collectionsehr groß ist und die Leistung für Sie von entscheidender Bedeutung ist, lohnt es sich, wieder die zwingenden und funktionalen Ansätze zu vergleichen und den für Sie geeigneten auszuwählen.

AUFGABE 2


Es gibt ein weiteres großartiges Beispiel für eine sehr präzise Verwendung einer Funktion höherer Ordnung reduce (_:, _:), auf die ich gestoßen bin. Dies ist ein SET- Spiel .
Hier sind die Grundregeln. Der Name des Spiels SETstammt vom englischen Wort "set" - "set". Das Spiel SETbesteht aus 81 Karten mit jeweils einem einzigartigen Bild:



Jede Karte hat 4 Attribute, die unten aufgeführt sind:

Anzahl : Jede Karte hat ein, zwei oder drei Zeichen.
Art der Zeichen : Ovale, Rauten oder Wellen.
Farbe : Symbole können rot, grün oder lila sein.
Füllen : Zeichen können leer, schattiert oder schattiert sein.

Zweck des SpielsSET: Unter den 12 Karten, die auf dem Tisch ausgelegt sind, müssen Sie SET(einen Satz) finden, der aus 3 Karten besteht, wobei jedes der Zeichen auf allen 3 Karten entweder vollständig übereinstimmt oder sich vollständig unterscheidet. Alle Zeichen müssen dieser Regel vollständig entsprechen.

Zum Beispiel sollte die Anzahl der Zeichen auf allen 3 Karten entweder gleich oder unterschiedlich sein, die Farbe auf allen 3 Karten sollte entweder gleich oder verschieden sein usw.

In diesem Beispiel werden wir uns nur für das Kartenmodell SET struct SetCardund den Algorithmus zur Bestimmung SETdurch interessieren 3. Karten isSet( cards:[SetCard]):

 struct SetCard: Equatable { let number: Variant // number - 1, 2, 3 let color: Variant // color - 1, 2, 3 (, , , ) let shape: Variant // symbol - 1, 2, 3 (, , , ) let fill: Variant // fill - 1, 2, 3 (, , , ) enum Variant: Int, CaseIterable { case v1 = 1 case v2 case v3 } static func isSet(cards: [SetCard]) -> Bool { guard cards.count == 3 else {return false} let sums = [ cards.reduce(0, { $0 + $1.number.rawValue }), cards.reduce(0, { $0 + $1.color.rawValue }), cards.reduce(0, { $0 + $1.shape.rawValue }), cards.reduce(0, { $0 + $1.fill.rawValue }) ] return sums.reduce(true, { $0 && ($1 % 3 == 0) }) } } 

Die Modelle jedes Merkmal - Nummer number , Symboltyp shape , Farbe color und Füllung fill - präsentiert Listing Variantdrei mögliche Werte aufweisen var1, var2und var3das entspricht die dritten ganzen Zahlen rawValue- 1,2,3. In dieser Form ist rawValuees einfach zu bedienen. Wenn wir zum Beispiel ein Zeichen nehmen und colordann alles rawValuefür colors3 Karten hinzufügen , werden wir feststellen, dass wenn colorsfür alle 3 Karten gleich ist, die Summe gleich 3ist 6oder 9, und wenn sie alle unterschiedlich sind, dann ist die Summe gleich gleich 6. In jedem dieser Fälle haben wir die Multiplizität der 3. Summe rawValuefürcolorsalle 3 Karten. Wir wissen, dass dies eine Voraussetzung dafür ist, was 3 Karten ausmachen SET. Damit 3 Karten SETfür alle Zeichen SetCard- Menge number, Symboltyp shape, Farbe colorund Füllung fill- wirklich notwendig werden, sollte ihre Summe rawValueein Vielfaches der 3 sein.

Daher wird in dem staticVerfahren, isSet( cards:[SetCard])wir zuerst das Array berechnen sumsSummen rawValuefür alle 3 Karten für alle 4 Leistungskarten Funktion höherer Ordnung unter Verwendung von reducemit einem anfänglichen Wert gleich 0, und Funktionen Akkumulieren {$0 + $1.number.rawValue}, {$0 + $1.color.rawValue}, {$0 + $1.shape.rawValue}, { {$0 + $1.fill.rawValue}. Jedes Element des Arrays sumsmuss ein Vielfaches von 3 sein, und wieder verwenden wir die Funktionreduce, diesmal jedoch mit einem Anfangswert, der der truelogischen Funktion " AND" entspricht und diese akkumuliert {$0 && ($1 % 3) == 0}. In Swift 5 wird isMultiply(of:)anstelle des %Restoperators eine Funktion eingeführt , um die Vielzahl einer Zahl für eine andere zu testen . Es wird auch die Lesbarkeit des Codes verbessern: { $0 && ($1.isMultiply(of:3) }.

Dieser fantastisch kurze Code, um herauszufinden, ob 3 SetCardKarten das SETi-te sind, wird dank des " funktionalen " Ansatzes erhalten, und wir können sicherstellen, dass er funktioniert Playground:



Wie man SETdie Benutzeroberfläche ( UI) auf diesem Spielmodell hier , hier und hier erstellt .

Reine Merkmale und Nebenwirkungen


Eine reine Funktion erfüllt zwei Bedingungen. Es wird immer das gleiche Ergebnis mit den gleichen Eingabeparametern zurückgegeben. Und die Berechnung des Ergebnisses verursacht keine Nebenwirkungen, die mit der Ausgabe von Daten außerhalb (z. B. auf die Festplatte) oder mit der Ausleihe von Quelldaten von außerhalb (z. B. Zeit) verbunden sind. Auf diese Weise können Sie den Code erheblich optimieren.

Dieses Thema ist in den ersten Folgen von " Funktionen " und " Nebenwirkungen " , die ins Russische übersetzt und als " Funktionen " und "Nebenwirkungen " dargestellt werden , Swiftperfekt auf point.free dargestellt .

Funktionszusammensetzung


In mathematischer Hinsicht bedeutet dies, eine Funktion auf das Ergebnis einer anderen Funktion anzuwenden. In einer SwiftFunktion können sie einen Wert zurückgeben, den Sie als Eingabe für eine andere Funktion verwenden können. Dies ist eine gängige Programmierpraxis.

Stellen Sie sich vor, wir haben ein Array von ganzen Zahlen und möchten ein Array von Quadraten mit eindeutigen geraden Zahlen am Ausgang erhalten. Normalerweise implementieren wir dies wie folgt neu:

 var integerArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 4, 5] func unique(_ array: [Int]) -> [Int] { return array.reduce(into: [], { (results, element) in if !results.contains(element) { results.append(element) } }) } func even(_ array: [Int]) -> [Int] { return array.filter{ $0%2 == 0} } func square(_ array: [Int]) -> [Int] { return array.map{ $0*$0 } } var array = square(even(unique(integerArray))) // it returns [4, 16, 36, 64] 

Dieser Code liefert das richtige Ergebnis, aber Sie sehen, dass die Lesbarkeit der letzten Codezeile nicht so einfach ist. Die Reihenfolge der Funktionen (von rechts nach links) ist das Gegenteil von der, an die wir gewöhnt sind (von links nach rechts) und die wir hier sehen möchten. Wir müssen unsere Logik zuerst auf den innersten Teil mehrerer Einbettungen richten - auf das Array inegerArray, dann auf die Funktion außerhalb dieses Arrays unique, dann auf eine andere Ebene - die Funktion evenund schließlich die Funktion in der Schlussfolgerung square.

Und hier hilft uns die „Zusammensetzung“ von Funktionen >>>und Operatoren |>, die es uns ermöglicht, den Code auf sehr bequeme Weise zu schreiben und die Verarbeitung des ursprünglichen Arrays integerArrayals „Förderer“ von Funktionen darzustellen :

 var array1 = integerArray |> unique >>> even >>> square 

Fast alle Sprachen wie funktionale Programmierung spezialisiert F#, Elixirund Elmdiese Operatoren für „Komposition“ -Funktionen verwenden.

Es Swiftgibt keine eingebauten Operatoren für die „Zusammensetzung“ von Funktionen >>>und |>, aber wir können sie sehr einfach mit Hilfe von GenericsClosures ( closure) und dem infixOperator abrufen:

 precedencegroup ForwardComposition{ associativity: left higherThan: ForwardApplication } infix operator >>> : ForwardComposition func >>> <A, B, C>(left: @escaping (A) -> B, right: @escaping (B) -> C) -> (A) -> C { return { right(left($0)) } } precedencegroup ForwardApplication { associativity: left } infix operator |> : ForwardApplication func |> <A, B>(a: A, f: (A) -> B) -> B { return f(a) } 

Trotz der zusätzlichen Kosten kann dies in einigen Fällen die Leistung, Lesbarkeit und Testbarkeit Ihres Codes erheblich verbessern. Wenn Sie beispielsweise mapeine ganze Funktionskette mit dem Operator "Komposition" platzieren, >>>anstatt ein Array durch zahlreiche zu jagen map:

 var integerArray1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 4, 5] let b = integerArray1.map( { $0 + 1 } >>> { $0 * 3 } >>> String.init) print (b) // ["6", "9", "12", "15", "18", "21", "24", "27", "30", "6", "15", "18"] 

Ein funktionaler Ansatz wirkt sich jedoch nicht immer positiv aus.

Als es Swift2014 erschien, beeilten sich alle, Bibliotheken mit Operatoren für die „Komposition“ von Funktionen zu schreiben und eine schwierige Aufgabe zu lösen, wie das Parsen JSONmit funktionalen Programmieroperatoren anstelle von unendlich verschachtelten Konstruktionen if let. Ich selbst habe den Artikel über das funktionale Parsen von JSON übersetzt, der mich mit seiner eleganten Lösung begeisterte und ein Fan der Argo- Bibliothek war .

Die Entwickler Swiftgingen jedoch einen völlig anderen Weg und schlugen auf der Grundlage der protokollorientierten Technologie eine viel präzisere Art des Schreibens von Code vor. Um die JSONDaten direkt an zu "liefern"Genug , um dies zu tun Codable, die automatisch dieses Protokoll implementiert, wenn Ihr Modell der bekannten besteht SwiftDatenstrukturen: String, Int, URL, Array, Dictionary, usw.

 struct Blog: Codable { let id: Int let name: String let url: URL } 

Mit JSONDaten von einem berühmten Artikel ...
 [ { "id" : 73, "name" : "Bloxus test", "url" : "http://remote.bloxus.com/" }, { "id" : 74, "name" : "Manila Test", "url" : "http://flickrtest1.userland.com/" } ] 

... im Moment benötigen Sie nur eine Codezeile, um eine Reihe von Blogs zu erhalten blogs:

 let blogs = Bundle.main.path(forResource: "blogs", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Blog].self, from: $0) } print ("\(blogs!)") // [id: 73 name: Bloxus test url: http://remote.bloxus.com/, // id: 74 name: Manila Test url: http://flickrtest1.userland.com/] 

Jeder hat sicher vergessen, die Operatoren der „Zusammensetzung“ von Funktionen zum Parsen zu verwenden JSON, wenn es eine andere, verständlichere und einfachere Möglichkeit gibt, dies mithilfe von Protokollen zu tun.

Wenn alles so einfach ist, können wir JSONDaten in komplexere Modelle „hochladen“ . Angenommen, wir haben eine Datendatei JSON, die einen Namen hat user.jsonund sich in unserem Verzeichnis befindet Resources.. Sie enthält Daten zu einem bestimmten Benutzer:

 { "email": "blob@pointfree.co", "id": 42, "name": "Blob" } 

Und wir haben einen Codable Benutzer Usermit einem Initialisierer aus den Daten json:

 struct User: Codable { let email: String let id: Int let name: String init?(json: Data) { if let newValue = try? JSONDecoder().decode(User.self, from: json) { self = newValue } else { return nil } } } 

Wir können sehr leicht einen neuen Benutzer newUsermit einem noch einfacheren Funktionscode finden:

 let newUser = Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { User.init(json: $0) } 

Offensichtlich wird der TYP newUsersein Optional, d.h.User? ::



Angenommen, wir Resourceshaben eine andere Datei mit einem Namen im Verzeichnis invoices.json, die Informationen zu den Rechnungen dieses Benutzers enthält.

 [ { "amountPaid": 1000, "amountDue": 0, "closed": true, "id": 1 }, { "amountPaid": 500, "amountDue": 500, "closed": false, "id": 2 } ] 

Wir können diese Daten genau so laden, wie wir es getan haben User. Definieren wir die Struktur als Rechnungsmodell struct Invoice...

 struct Invoice: Codable { let amountDue: Int let amountPaid: Int let closed: Bool let id: Int } 

... und dekodieren Sie das oben dargestellte JSONRechnungsarray invoices, indem Sie nur den Dateipfad und die Dekodierungslogik ändern decode:

 let invoices = Bundle.main.path(forResource: "invoices", ofType: "json") .map( URL.init(fileURLWithPath:) ) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } 

invoices wird sein [Invoice]? ::



Jetzt möchten wir den Benutzer usermit seinen Rechnungen verbinden invoices, wenn diese nicht gleich sind nil, und beispielsweise in der Struktur des Umschlags speichern, UserEnvelopeder zusammen mit seinen Rechnungen an den Benutzer gesendet wird:

 struct UserEnvelope { let user: User let invoices: [Invoice] } 

Anstatt zweimal aufzutreten if let...

 if let newUser = newUser, let invoices = invoices { } 

... sie schreibt ein funktionelles Analogdoppel if letals GenericHilfsfunktion zip, die zwei Transformieren OptionalWerte in Optionaleinem Tupel:

 func zip<A, B>(_ a: A?, _ b: B?) -> (A, B)? { if let a = a, let b = b { return (a, b) } return nil } 

Jetzt haben wir keinen Grund, den Variablen etwas zuzuweisen, newUserund invoiceswir bauen einfach alles in unsere neue Funktion ein zip, verwenden den Initialisierer UserEnvelope.initund alles wird funktionieren!

 let userEnv = zip( Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { User.init(json: $0) }, Bundle.main.path(forResource: "invoices", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } ).flatMap (UserEnvelope.init) print ("\(userEnv!)") // UserEnvelope(user: id: 42 name: Blob , // invoices: [id: 1 amountDue: 0 amountPaid: 1000 closed: true, // id: 2 amountDue: 500 amountPaid: 500 closed: false]) 

In einem einzigen Ausdruck wird ein ganzer Algorithmus zum Liefern von JSONDaten an einen komplexen in Form einer Struktur gepackt struct UserEnvelope.

  • zip , , . user , JSON , invoices , JSON . .
  • map , , «» .
  • flatMap , , , .

Der Betrieb zip, mapund flatMapist svoebrazny domänenspezifische Sprache (domänenspezifische Sprache, DSL) für Daten zu konvertieren.

Wir können diese Demo weiterentwickeln, um das asynchrone Lesen des Inhalts einer Datei als eine spezielle Funktion darzustellen , die Sie auf pointfree.co sehen können .

Ich bin kein fanatischer Fan der funktionalen Programmierung überall und in allem, aber eine moderate Verwendung scheint mir ratsam.

Fazit


Ich habe Beispiele für verschiedene funktionale Programmierung bieten Swft «out of the box“, basierend auf der Verwendung von Funktionen höherer Ordnung map, flatMap, reduce, filterund die andere für die Sequenzen Sequence, Optionalund Result. Sie können „Arbeitspferde“ seinen Code erzeugen in ,besonders dann , wenn der Wert beteiligt ist - Strukturen structund Aufzählungen enum. Ein iOSAnwendungsentwickler muss dieses Tool besitzen.

Alle zusammengestellten Demos Playgroundfinden Sie auf Github . Wenn Sie Probleme mit dem Start haben Playground, können Sie diesen Artikel sehen:

So entfernen Sie Xcode Playground-Fehler mit den Meldungen "Launching Simulator" und "Running Playground".

Referenzen:

Functional Programming in Swift: An Introduction.
An Introduction to Functional Programming in Swift.
The Many Faces of Flat-Map: Part 3
Inside the Standard Library: Sequence.map()
Practical functional programming in Swift

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


All Articles