Machen Sie Ihren Android-Code zukunftssicher, Teil 1: Grundlagen der funktionalen und reaktiven Programmierung

Veröffentlicht: 2022-08-31

Das Schreiben von sauberem Code kann eine Herausforderung darstellen: Bibliotheken, Frameworks und APIs sind temporär und veralten schnell. Aber mathematische Konzepte und Paradigmen sind dauerhaft; Sie erfordern jahrelange akademische Forschung und können uns sogar überdauern.

Dies ist kein Tutorial, das Ihnen zeigt, wie Sie X mit Bibliothek Y machen. Stattdessen konzentrieren wir uns auf die dauerhaften Prinzipien hinter funktionaler und reaktiver Programmierung, damit Sie eine zukunftssichere und zuverlässige Android-Architektur erstellen und skalieren und sich an Änderungen anpassen können, ohne Kompromisse einzugehen Effizienz.

Dieser Artikel legt die Grundlagen, und in Teil 2 tauchen wir in eine Implementierung der funktionalen reaktiven Programmierung (FRP) ein, die funktionale und reaktive Programmierung kombiniert.

Dieser Artikel wurde für Android-Entwickler geschrieben, aber die Konzepte sind relevant und nützlich für alle Entwickler mit Erfahrung in allgemeinen Programmiersprachen.

Funktionale Programmierung 101

Funktionale Programmierung (FP) ist ein Muster, bei dem Sie Ihr Programm als eine Zusammensetzung von Funktionen erstellen und Daten von $A$ in $B$, in $C$ usw. umwandeln, bis die gewünschte Ausgabe erreicht ist. Bei der objektorientierten Programmierung (OOP) teilen Sie dem Computer Anweisung für Anweisung mit, was er tun soll. Funktionale Programmierung ist anders: Sie geben den Kontrollfluss auf und definieren stattdessen ein „Rezept von Funktionen“, um Ihr Ergebnis zu erzeugen.

Ein grünes Rechteck auf der linken Seite mit dem Text "Input: x" hat einen Pfeil, der auf ein hellgraues Rechteck mit der Aufschrift "Function: f" zeigt. Innerhalb des hellgrauen Rechtecks ​​befinden sich drei Zylinder mit nach rechts zeigenden Pfeilen: Der erste ist hellblau mit der Aufschrift „A(x)“, der zweite ist dunkelblau mit der Aufschrift „B(x)“ und der dritte ist dunkelgrau mit der Aufschrift „C“. (x)." Rechts neben dem hellgrauen Rechteck befindet sich ein grünes Rechteck mit dem Text „Output: f(x).“ Am unteren Rand des hellgrauen Rechtecks ​​befindet sich ein Pfeil, der nach unten auf den Text „Nebenwirkungen“ zeigt.
Das funktionale Programmiermuster

FP stammt aus der Mathematik, insbesondere dem Lambda-Kalkül, einem logischen System der Funktionsabstraktion. Anstelle von OOP-Konzepten wie Schleifen, Klassen, Polymorphismus oder Vererbung befasst sich FP ausschließlich mit Abstraktion und Funktionen höherer Ordnung, mathematischen Funktionen, die andere Funktionen als Eingabe akzeptieren.

Kurz gesagt, FP hat zwei Hauptakteure: Daten (das Modell oder die für Ihr Problem erforderlichen Informationen) und Funktionen (Darstellungen des Verhaltens und der Transformationen zwischen Daten). Im Gegensatz dazu binden OOP-Klassen explizit eine bestimmte domänenspezifische Datenstruktur – und die jeder Klasseninstanz zugeordneten Werte oder Zustände – an Verhaltensweisen (Methoden), die dazu bestimmt sind, damit verwendet zu werden.

Wir werden drei Schlüsselaspekte von FP näher untersuchen:

  • FP ist deklarativ.
  • FP verwendet Funktionskomposition.
  • FP-Funktionen sind rein.

Ein guter Ausgangspunkt, um weiter in die FP-Welt einzutauchen, ist Haskell, eine stark typisierte, rein funktionale Sprache. Ich empfehle Learn You a Haskell for Great Good! interaktives Tutorial als nützliche Ressource.

FP-Bestandteil Nr. 1: Deklarative Programmierung

Das erste, was Ihnen an einem FP-Programm auffallen wird, ist, dass es im deklarativen und nicht im imperativen Stil geschrieben ist. Kurz gesagt, die deklarative Programmierung teilt einem Programm mit, was zu tun ist, anstatt wie es zu tun ist. Lassen Sie uns diese abstrakte Definition mit einem konkreten Beispiel für imperative versus deklarative Programmierung begründen, um das folgende Problem zu lösen: Geben Sie eine Liste von Namen zurück, die nur die Namen mit mindestens drei Vokalen enthält, wobei die Vokale in Großbuchstaben angezeigt werden.

Imperative Lösung

Lassen Sie uns zunächst die zwingende Lösung dieses Problems in Kotlin untersuchen:

 fun namesImperative(input: List<String>): List<String> { val result = mutableListOf<String>() val vowels = listOf('A', 'E', 'I', 'O', 'U','a', 'e', 'i', 'o', 'u') for (name in input) { // loop 1 var vowelsCount = 0 for (char in name) { // loop 2 if (isVowel(char, vowels)) { vowelsCount++ if (vowelsCount == 3) { val uppercaseName = StringBuilder() for (finalChar in name) { // loop 3 var transformedChar = finalChar // ignore that the first letter might be uppercase if (isVowel(finalChar, vowels)) { transformedChar = finalChar.uppercaseChar() } uppercaseName.append(transformedChar) } result.add(uppercaseName.toString()) break } } } } return result } fun isVowel(char: Char, vowels: List<Char>): Boolean { return vowels.contains(char) } fun main() { println(namesImperative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"))) // [IlIyAn, AnnAbEl, NIcOlE] }

Wir werden nun unsere zwingende Lösung unter Berücksichtigung einiger wichtiger Entwicklungsfaktoren analysieren:

  • Am effizientesten: Diese Lösung hat eine optimale Speichernutzung und eine gute Leistung bei der Big O-Analyse (basierend auf einer minimalen Anzahl von Vergleichen). In diesem Algorithmus ist es sinnvoll, die Anzahl der Vergleiche zwischen Zeichen zu analysieren, da dies die vorherrschende Operation in unserem Algorithmus ist. Sei $n$ die Anzahl der Namen und $k$ die durchschnittliche Länge der Namen.

    • Anzahl der Vergleiche im ungünstigsten Fall: $n(10k)(10k) = 100nk^2$
    • Erklärung: $n$ (Schleife 1) * $10k$ (für jedes Zeichen vergleichen wir 10 mögliche Vokale) * $10k$ (wir führen die isVowel() erneut aus, um zu entscheiden, ob das Zeichen in Großbuchstaben geschrieben werden soll – wieder in der im schlimmsten Fall vergleicht dies mit 10 Vokalen).
    • Ergebnis: Da die durchschnittliche Namenslänge nicht mehr als 100 Zeichen beträgt, können wir sagen, dass unser Algorithmus in $O(n)$ -Zeit läuft.
  • Komplex mit schlechter Lesbarkeit: Verglichen mit der deklarativen Lösung, die wir als Nächstes betrachten werden, ist diese Lösung viel länger und schwerer nachzuvollziehen.
  • Fehleranfällig: Der Code mutiert result , vowelsCount und transformedChar ; Diese Zustandsänderungen können zu subtilen Fehlern führen, z. B. wenn vergessen wird, vowelsCount auf 0 zurückzusetzen. Der Ausführungsablauf kann auch kompliziert werden, und es ist leicht zu vergessen, die break -Anweisung in der dritten Schleife hinzuzufügen.
  • Schlechte Wartbarkeit: Da unser Code komplex und fehleranfällig ist, kann es schwierig sein, das Verhalten dieses Codes umzugestalten oder zu ändern. Wenn zum Beispiel das Problem modifiziert würde, um Namen mit drei Vokalen und fünf Konsonanten auszuwählen, müssten wir neue Variablen einführen und die Schleifen ändern, was viele Möglichkeiten für Fehler lassen würde.

Unsere Beispiellösung veranschaulicht, wie komplexer imperativer Code aussehen könnte, obwohl Sie den Code verbessern könnten, indem Sie ihn in kleinere Funktionen umgestalten.

Deklarative Lösung

Nachdem wir nun verstanden haben, was deklarative Programmierung nicht ist , wollen wir unsere deklarative Lösung in Kotlin enthüllen:

 fun namesDeclarative(input: List<String>): List<String> = input.filter { name -> name.count(::isVowel) >= 3 }.map { name -> name.map { char -> if (isVowel(char)) char.uppercaseChar() else char }.joinToString("") } fun isVowel(char: Char): Boolean = listOf('A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u').contains(char) fun main() { println(namesDeclarative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"))) // [IlIyAn, AnnAbEl, NIcOlE] }

Unter Verwendung der gleichen Kriterien, die wir zur Bewertung unserer imperativen Lösung verwendet haben, sehen wir uns an, wie sich der deklarative Code hält:

  • Effizient: Die Imperativ- und Deklarativimplementierungen laufen beide in linearer Zeit, aber die Imperativimplementierung ist etwas effizienter, weil ich hier name.count() verwendet habe, das die Vokale bis zum Ende des Namens weiterzählt (auch nachdem drei Vokale gefunden wurden). ). Wir können dieses Problem leicht beheben, indem wir eine einfache hasThreeVowels(String): Boolean Funktion schreiben. Diese Lösung verwendet denselben Algorithmus wie die imperative Lösung, daher gilt hier dieselbe Komplexitätsanalyse: Unser Algorithmus läuft in $O(n)$- Zeit.
  • Prägnant und gut lesbar: Die imperative Lösung hat 44 Zeilen mit großem Einzug im Vergleich zu unserer deklarativen Lösung von 16 Zeilen mit kleinem Einzug. Zeilen und Tabulatoren sind nicht alles, aber ein Blick auf die beiden Dateien zeigt, dass unsere deklarative Lösung viel besser lesbar ist.
  • Weniger fehleranfällig: In diesem Beispiel ist alles unveränderlich. Wir transformieren eine List<String> aller Namen in eine List<String> von Namen mit drei oder mehr Vokalen und transformieren dann jedes String Wort in ein String Wort mit Vokalen in Großbuchstaben. Insgesamt macht das Fehlen von Mutationen, verschachtelten Schleifen oder Unterbrechungen und das Aufgeben des Kontrollflusses den Code einfacher und bietet weniger Raum für Fehler.
  • Gute Wartbarkeit: Sie können deklarativen Code aufgrund seiner Lesbarkeit und Robustheit leicht umgestalten. In unserem vorherigen Beispiel (sagen wir, das Problem wurde geändert, um Namen mit drei Vokalen und fünf Konsonanten auszuwählen) wäre eine einfache Lösung, die folgenden Anweisungen in die filter einzufügen: val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5 val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5 .

Als zusätzliches Plus ist unsere deklarative Lösung rein funktional: Jede Funktion in diesem Beispiel ist rein und hat keine Nebenwirkungen. (Mehr über Reinheit später.)

Bonus deklarative Lösung

Werfen wir einen Blick auf die deklarative Implementierung desselben Problems in einer rein funktionalen Sprache wie Haskell, um zu demonstrieren, wie es sich liest. Wenn Sie mit Haskell nicht vertraut sind, beachten Sie, dass die . Der Operator in Haskell lautet „nachher“. Beispiel: solution = map uppercaseVowels . filter hasThreeVowels solution = map uppercaseVowels . filter hasThreeVowels bedeutet „Vokale in Großbuchstaben abbilden, nachdem nach Namen mit drei Vokalen gefiltert wurde“.

 import Data.Char(toUpper) namesSolution :: [String] -> [String] namesSolution = map uppercaseVowels . filter hasThreeVowels hasThreeVowels :: String -> Bool hasThreeVowels s = count isVowel s >= 3 uppercaseVowels :: String -> String uppercaseVowels = map uppercaseVowel where uppercaseVowel :: Char -> Char uppercaseVowel c | isVowel c = toUpper c | otherwise = c isVowel :: Char -> Bool isVowel c = c `elem` vowels vowels :: [Char] vowels = ['A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u'] count :: (a -> Bool) -> [a] -> Int count _ [] = 0 count pred (x:xs) | pred x = 1 + count pred xs | otherwise = count pred xs main :: IO () main = print $ namesSolution ["Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"] -- ["IlIyAn","AnnAbEl","NIcOlE"]

Diese Lösung verhält sich ähnlich wie unsere deklarative Kotlin-Lösung, mit einigen zusätzlichen Vorteilen: Sie ist lesbar, einfach, wenn Sie die Syntax von Haskell verstehen, rein funktional und faul.

Die zentralen Thesen

Die deklarative Programmierung ist sowohl für die FP- als auch für die reaktive Programmierung nützlich (die wir in einem späteren Abschnitt behandeln werden).

  • Es beschreibt „was“ Sie erreichen wollen – und nicht „wie“ Sie es erreichen, mit der genauen Reihenfolge der Ausführung von Anweisungen.
  • Es abstrahiert den Kontrollfluss eines Programms und konzentriert sich stattdessen auf das Problem in Form von Transformationen (dh $A \rightarrow B \rightarrow C \rightarrow D$).
  • Es fördert weniger komplexen, prägnanteren und besser lesbaren Code, der sich leichter umgestalten und ändern lässt. Wenn sich Ihr Android-Code nicht wie ein Satz liest, machen Sie wahrscheinlich etwas falsch.

Wenn sich Ihr Android-Code nicht wie ein Satz liest, machen Sie wahrscheinlich etwas falsch.

Twittern

Dennoch hat die deklarative Programmierung gewisse Nachteile. Es ist möglich, dass ineffizienter Code entsteht, der mehr RAM verbraucht und schlechter abschneidet als eine zwingende Implementierung. Sortieren, Backpropagation (beim maschinellen Lernen) und andere „mutierende Algorithmen“ passen nicht gut zum unveränderlichen, deklarativen Programmierstil.

FP-Bestandteil Nr. 2: Funktionszusammensetzung

Die Funktionskomposition ist das mathematische Konzept im Herzen der funktionalen Programmierung. Wenn die Funktion $f$ $A$ als Eingabe akzeptiert und $B$ als Ausgabe erzeugt ($f: A \rightarrow B$), und die Funktion $g$ $B$ akzeptiert und $C$ ($g: B \rightarrow C$), dann können Sie eine dritte Funktion, $h$, erstellen, die $A$ akzeptiert und $C$ erzeugt ($h: A \rightarrow C$). Wir können diese dritte Funktion als die Zusammensetzung von $g$ mit $f$ definieren, auch notiert als $g \circ f$ oder $g(f())$:

Ein blaues Kästchen mit der Bezeichnung „A“ hat einen Pfeil „f“, der auf ein blaues Kästchen mit der Bezeichnung „B“ zeigt, das einen Pfeil „g“ hat, der auf ein blaues Kästchen mit der Bezeichnung „C“ zeigt. Kästchen „A“ hat auch einen parallelen Pfeil, „g of“, der direkt auf Kästchen „C“ zeigt.
Funktionen f, g und h, die Zusammensetzung von g mit f.

Jede imperative Lösung kann in eine deklarative Lösung übersetzt werden, indem das Problem in kleinere Probleme zerlegt, diese unabhängig voneinander gelöst und die kleineren Lösungen durch Funktionskomposition zur endgültigen Lösung wieder zusammengesetzt werden. Schauen wir uns unser Namensproblem aus dem vorherigen Abschnitt an, um dieses Konzept in Aktion zu sehen. Unsere kleineren Probleme aus der imperativen Lösung sind:

  1. isVowel :: Char -> Bool : Gibt bei einem Char zurück, ob es ein Vokal ist oder nicht ( Bool ).
  2. countVowels :: String -> Int : Gibt bei einem String die Anzahl der darin enthaltenen Vokale zurück ( Int ).
  3. hasThreeVowels :: String -> Bool : Gibt bei einem String zurück, ob er mindestens drei Vokale hat ( Bool ).
  4. uppercaseVowels :: String -> String : Gibt bei einem String einen neuen String mit Vokalen in Großbuchstaben zurück.

Unsere deklarative Lösung, die durch Funktionskomposition erreicht wird, ist map uppercaseVowels . filter hasThreeVowels map uppercaseVowels . filter hasThreeVowels .

Ein oberes Diagramm hat drei blaue "[String]"-Kästchen, die durch Pfeile verbunden sind, die nach rechts zeigen. Der erste Pfeil ist mit „filter has3Vowels“ und der zweite mit „mapuppercasevowels“ beschriftet. Unten, ein zweites Diagramm hat zwei blaue Kästchen auf der linken Seite, "Char" oben und "String" unten, die auf ein blaues Kästchen auf der rechten Seite zeigen, "Bool". Der Pfeil von „Char“ zu „Bool“ ist mit „isVowel“ und der Pfeil von „String“ zu „Bool“ mit „has3Vowels“ gekennzeichnet. Das Feld „String“ hat auch einen Pfeil, der auf sich selbst zeigt und mit „uppercaseVowels“ beschriftet ist.
Ein Beispiel für die Zusammensetzung von Funktionen unter Verwendung unseres Namensproblems.

Dieses Beispiel ist etwas komplizierter als eine einfache $A \rightarrow B \rightarrow C$-Formel, aber es demonstriert das Prinzip der Funktionskomposition.

Die zentralen Thesen

Die Funktionskomposition ist ein einfaches, aber leistungsstarkes Konzept.

  • Es bietet eine Strategie zur Lösung komplexer Probleme, bei der Probleme in kleinere, einfachere Schritte aufgeteilt und zu einer Lösung kombiniert werden.
  • Es bietet Bausteine, mit denen Sie Teile der endgültigen Lösung einfach hinzufügen, entfernen oder ändern können, ohne sich Sorgen machen zu müssen, dass etwas kaputt geht.
  • Sie können $g(f())$ zusammensetzen, wenn die Ausgabe von $f$ mit dem Eingabetyp von $g$ übereinstimmt.

Beim Erstellen von Funktionen können Sie nicht nur Daten, sondern auch Funktionen als Eingabe an andere Funktionen übergeben – ein Beispiel für Funktionen höherer Ordnung.

FP-Bestandteil Nr. 3: Reinheit

Es gibt ein weiteres Schlüsselelement der Funktionskomposition, das wir ansprechen müssen: Die Funktionen, die Sie zusammensetzen, müssen rein sein, ein weiteres Konzept, das aus der Mathematik stammt. In der Mathematik sind alle Funktionen Berechnungen, die immer dieselbe Ausgabe liefern, wenn sie mit derselben Eingabe aufgerufen werden; dies ist die Grundlage der Reinheit.

Sehen wir uns ein Pseudocode-Beispiel mit mathematischen Funktionen an. Angenommen, wir haben eine Funktion, makeEven , die eine ganzzahlige Eingabe verdoppelt, um sie gerade zu machen, und dass unser Code die Zeile makeEven(x) + x unter Verwendung der Eingabe x = 2 ausführt. In der Mathematik würde diese Berechnung immer zu einer Berechnung von $2x + x = 3x = 3(2) = 6$ führen und ist eine reine Funktion. Dies ist jedoch beim Programmieren nicht immer der Fall – wenn die Funktion makeEven(x) x durch Verdoppelung mutiert, bevor der Code unser Ergebnis zurückgibt, dann würde unsere Zeile $2x + (2x) = 4x = 4(2) = 8$ berechnen und schlimmer noch, das Ergebnis würde sich mit jedem makeEven -Aufruf ändern.

Lassen Sie uns einige Arten von Funktionen untersuchen, die nicht rein sind, uns aber dabei helfen, Reinheit genauer zu definieren:

  • Teilfunktionen: Dies sind Funktionen, die nicht für alle Eingabewerte definiert sind, wie z. B. die Division. Aus Programmiersicht sind dies Funktionen, die eine Ausnahme auslösen: fun divide(a: Int, b: Int): Float löst eine ArithmeticException für die Eingabe b = 0 aus, die durch die Division durch Null verursacht wird.
  • Gesamtfunktionen: Diese Funktionen sind für alle Eingabewerte definiert, können jedoch eine andere Ausgabe oder Seiteneffekte erzeugen, wenn sie mit derselben Eingabe aufgerufen werden. Die Android-Welt ist voll von Gesamtfunktionen: Log.d , LocalDateTime.now und Locale.getDefault sind nur einige Beispiele.

Unter Berücksichtigung dieser Definitionen können wir reine Funktionen als Gesamtfunktionen ohne Nebenwirkungen definieren. Funktionskompositionen, die nur mit reinen Funktionen erstellt wurden, erzeugen zuverlässigeren, vorhersagbaren und testbaren Code.

Tipp: Um eine Gesamtfunktion rein zu machen, können Sie ihre Nebenwirkungen abstrahieren, indem Sie sie als Funktionsparameter höherer Ordnung übergeben. Auf diese Weise können Sie ganz einfach Gesamtfunktionen testen, indem Sie eine verspottete Funktion höherer Ordnung übergeben. Dieses Beispiel verwendet die Annotation @SideEffect aus einer Bibliothek, die wir später im Tutorial untersuchen, Ivy FRP:

 suspend fun deadlinePassed( deadline: LocalDate, @SideEffect currentDate: suspend () -> LocalDate ): Boolean = deadline.isAfter(currentDate())

Die zentralen Thesen

Reinheit ist die letzte Zutat, die für das Paradigma der funktionalen Programmierung erforderlich ist.

  • Seien Sie vorsichtig mit Teilfunktionen – sie können Ihre App zum Absturz bringen.
  • Das Zusammensetzen von Gesamtfunktionen ist nicht deterministisch; es kann zu unvorhersehbarem Verhalten führen.
  • Schreiben Sie nach Möglichkeit reine Funktionen. Sie profitieren von erhöhter Codestabilität.

Lassen Sie uns nach Abschluss unseres Überblicks über die funktionale Programmierung die nächste Komponente des zukunftssicheren Android-Codes untersuchen: die reaktive Programmierung.

Reaktive Programmierung 101

Reaktive Programmierung ist ein deklaratives Programmiermuster, bei dem das Programm auf Daten- oder Ereignisänderungen reagiert, anstatt Informationen über Änderungen anzufordern.

Zwei blaue Hauptfelder, „Observable“ und „State“, haben zwei Hauptpfade zwischen sich. Die erste ist über "Beobachtet (überwacht Änderungen)." Die zweite geht über „Benachrichtigt (über den neuesten Stand)“ zum blauen Kästchen „UI (API im Backend)“, das über „Transforms user input to“ zum blauen Kästchen „Event“ geht, das über „Triggers“ zum blauen geht Feld „Funktionszusammenstellung“ und schließlich über „Erzeugt (Neuzustand)“. "State" verbindet sich dann auch wieder mit "Function Composition" über "Acts as input for".
Der allgemeine reaktive Programmierzyklus.

Die Grundelemente in einem reaktiven Programmierzyklus sind Ereignisse, die deklarative Pipeline, Zustände und Observables:

  • Ereignisse sind Signale von der Außenwelt, typischerweise in Form von Benutzereingaben oder Systemereignissen, die Aktualisierungen auslösen. Der Zweck eines Ereignisses besteht darin, ein Signal in eine Pipeline-Eingabe umzuwandeln.
  • Die deklarative Pipeline ist eine Funktionskomposition, die (Event, State) als Eingabe akzeptiert und diese Eingabe in einen neuen State (die Ausgabe) umwandelt: (Event, State) -> f -> g -> … -> n -> State . Pipelines müssen asynchron ausgeführt werden, um mehrere Ereignisse zu verarbeiten, ohne andere Pipelines zu blockieren oder auf deren Beendigung zu warten.
  • Zustände sind die Darstellung der Softwareanwendung durch das Datenmodell zu einem bestimmten Zeitpunkt. Die Domänenlogik verwendet den Zustand, um den gewünschten nächsten Zustand zu berechnen und entsprechende Aktualisierungen vorzunehmen.
  • Observables hören auf Zustandsänderungen und informieren Abonnenten über diese Änderungen. In Android werden Observables normalerweise mit Flow , LiveData oder RxJava und benachrichtigen die Benutzeroberfläche über Zustandsaktualisierungen, damit sie entsprechend reagieren kann.

Es gibt viele Definitionen und Implementierungen der reaktiven Programmierung. Hier habe ich einen pragmatischen Ansatz gewählt, der sich darauf konzentriert, diese Konzepte auf reale Projekte anzuwenden.

Die Punkte verbinden: Funktionale reaktive Programmierung

Funktionale und reaktive Programmierung sind zwei mächtige Paradigmen. Diese Konzepte reichen über die kurzlebige Lebensdauer von Bibliotheken und APIs hinaus und werden Ihre Programmierkenntnisse für die kommenden Jahre verbessern.

Darüber hinaus vervielfacht sich die Leistungsfähigkeit von FP und reaktiver Programmierung, wenn sie kombiniert werden. Jetzt, da wir klare Definitionen von funktionaler und reaktiver Programmierung haben, können wir die Teile zusammensetzen. In Teil 2 dieses Tutorials definieren wir das Paradigma der funktionalen reaktiven Programmierung (FRP) und setzen es mit einer Beispiel-App-Implementierung und relevanten Android-Bibliotheken in die Praxis um.

Der Toptal Engineering Blog dankt Tarun Goyal für die Überprüfung der in diesem Artikel vorgestellten Codebeispiele.