Reaktive Programmierung hat sich über RxJS in der Web-Entwicklung als Standardentwicklungsmuster etabliert. RxJS selber ist zwar sehr mächtig aber gleichzeitig auch sehr komplex und damit anfällig für Fehler, die aus Unverständnis entstehen. Alleine die Unterscheidung zwischen heißen, kalten und lauwarmen Strömen können einen Entwickler bei dem ersten Kontakt mit dem Framework nachhaltig verwirren.
Die funktionale reaktive Programmierung (FRP) stellt eine Variante reaktiver Programmierung dar. Sie basiert auf einem vergleichsweise kleinen und stringentem Satz an Basisoperationen und Kombinatoren. Diese ermöglichen es, komplexe GUI-Logik modular zu implementieren und dabei typische Fehlerklassen bei der GUI-Entwicklung auszuschließen.
In diesem Vortrag wird FRP anhand des Open-Source Frameworks Sodiums vorgestellt.
2. Zu meiner Person
• 2003: Promotion an der FU zum Thema
SW-Architekturen und Verteilte Systeme
• Ab 2004: Berater bei der akquinet
• technischer Architekt in J2EE-Projekten
• Projektleitung
• Betriebsführung, Wartung
• klassische Beratung und Schulungen
• 2006-2010:
Leiter des JBoss-Competence-Centers bei der akquinet
• Ab 2011:
Geschäftsführer der akquinet tech@spree
5. Funktionale GUIs – ein Oxymoron?
„Reine funktionale Sprachen (z.B. Haskell)
§haben keine Seiteneffekte
§=> f(x) ist für festes x konstant
„Problematisch sind damit, z.B.
leseDateneingabeVomBenutzer()
leseAktuelleMausposition()
leseFormularfelder()
6. Dann kam...
„Erste Version: 0.1.0.0 – März 2011
„Aktuelle Version: 1.1.0.1 – Mai 2016
„Nur GUI-Logik, ohne GUI-Anbindung
=> Im Prinzip fertig
„three-penny-gui: Browser als GUI
Reactive Banana
8. Problem gelöst?
„Prinzipiell schon, aber..
§Framework für Nicht-Haskell-Experten
nicht leicht zugänglich
„Aus einem Tutorial
How?
variation in time as first-class value
type Behavior a = Time → a
type Event a = [(Time, a)]
key data types are Behavior and Event.
vior corresponds to a „value that varies in time“.
t corresponds to „events that occurr at certains points in time“.
Behavior API
instance Applicative Behavior
Functor
instance Functor Behavior
9. Subjektive Bewertung
„GUI-Problem für funktionale Sprachen damit prinzipiell gelöst.
„Stringente Architektur mit einfachen Primitiven,
§die komplexe
§interaktive und aktive
§Oberflächen ermöglicht.
10. Parallel/leicht verzögert entwickelte sich ...
„The Reactive Manifesto (V1.0 – 2013, V2.0 - 2014)
https://www.reactivemanifesto.org
§Architekturprinzipien für antwortbereite (responsive),
widerstandsfähige (resilient), elastische, nachrichtenorientierte
Systeme
„Reactive Extensions (Rx)
§Erweiterung von LINQ (Abfragesprache für .NET) für
Ereignissteuerung ohne Rückrufmethoden basierend auf
Observer-Muster
http://reactivex.io/intro.html
(vor 2012 entwickelt, konstante Weiterentwicklung)
11. Und auch dieses hier...
„Von Stephen Blackheath, Anthony
Jones; 2016
„FRP als Architekturstil
mit Schwerpunkt auf Sodium
=> eigenes Framework
„IMHO:
Erfahrene GUI-Pragmatiker entdecken
FRP im Reactive-Umfeld neu
(späterer Abgleich mit Reactive Banana)
12. Worum geht es hier?
„Framework
§mit theoretisch beweisbaren
Eigenschaften
§um Fehlerklassen zu eliminieren,
§die bei klassischer Callback-basierten GUIs
auftreten
§unabhängig von GUI-Framework.
„... klingt erstmal nach Elfenbeinturm
13. Die 6 Plagen der Listener
Aus dem FRP-Buch von
S. Blackheath, A. Jones
entnommen
14. Unpredictable Order of Events
„Ausführungsreihenfolge von Listenern
hängt ab von Registrierungsreihenfolge
„bei FRP:
§Reihenfolge der Bearbeitung ist nicht
relevant,
§da unabhängige Berechnungen sich
nicht „sehen“
§aufgrund von unveränderbaren Daten
und GUI-TX
15. Missed First Events
„Problem, dass Events verschickt
werden, bevor alle Listener registriert
sind
(z.B. in der Initialisierungsphase)
„Bei FRP:
Zuerst werden alle “Listener“
konfiguriert, bevor Events verschickt
werden
16. Messy State
„Listener-basierter Code orientiert sich
üblicherweise an Zustandsmaschinen
(z.B. Entität unverändert/verändert,
Feld gültig/ungültig)
„Dieser ist mit Zustand und
Zustandsübergängen im Code sehr
verteilt und wird schnell unwartbar
„Bei FRP:
Funktionales Ordnen von Zuständen
und Übergängen
17. Threading Issues
„Gefahr von Deadlock in einem
Multihreading-System
„Bei FRP
§keine inhärenten Listener, sondern
Event-Ströme, die sich nicht in die
Quere kommen können
§+ Immutable State, so dass keine
Synchronisierung notwendig ist
(innerhalb des FRPs)
18. Leaking Callbacks
„Speicherlöcher durch nicht
deregistrierte Listener
„Registrierung von Listenern ist schon
schwierig, umso mehr die
Deregistrierung
„Bei FRP
keine Listener, sondern Event-Ströme,
die in Gänze aus dem Scope fallen und
aufgeräumt werden
19. Accidental Recursion
„Die Reihenfolge, in der Zustand verändert
wird und Events ausgelöst werden, ist
wichtig, Fehler können zu Endlosschleifen
und Inkonsistenzen führen.
„Bei FRP:
§komplette Analyse der Eventströme
§+ Transaktionsmodell
§= keine Rekursion/Zyklen, keine
Inkonsistenzen
21. Die zwei Basiselemente von Sodium
„Zellen (Cell<T>)
§Speichern/Verwalten von Zustand, der sich über
Zeit ändert
(alle anderen Daten/Objekte sind unveränderbar)
§Hält ein Element vom Typ T
„Ströme (Stream<T>)
§Verwalten von Aktivitäten
§Überträgt ein Ereignis vom Typ T
Zelle
Strom
22. Der Taschenrechner
„War zu optimistische Übungsaufgabe in Java
„Fähigkeiten
§Eingabe von Zahlen
§Mehrstufige Additionen und Subtraktionen
§Abschluss einer Berechnung
„GUI in Java FX
„Der Code:
https://github.com/akquinet/sodiumcalculat
or
23. Die (grobe) Architektur
„CalculatorView
§Erzeugt GUI
§Delegiert Aktionen an den Controller
§Angemeldet an der DisplayCell für die
Darstellung der Anzeige
„CalculatorController
§GUI – Status
§GUI – Logik
24. Ankopplung von FRP mit Sodium
„ Registrieren an einer Zelle in der GUI
// Bibliothek für Hilfsfunktionen außerhalb FRP
Operational
// Konvertiert Cell in Strom von Zuständen
.updates(controller.getDisplayCell())
// registriert Listener
.listen(v -> displayTF.setText("" + v));
„ Senden von Ereignissen außerhalb FRP
void pressDigit(Long digit) {
clickedDigitS.send(digit);
}
„ Achtung: Dies hier ist außerhalb von FRP, die
6 Plagen existieren hier.
25. Feature 1: Die Anzeige
„Zelle Display – hält den Wert, der angezeigt werden soll
„Klick auf Taste dig: display -> display*10+dig
“3“ “2“
26. In FRP als Diagramm – Schritt 1
„Anwender drückt eine Zahl 3, diese wird von außen reingegeben
„TX startet, 3 wird propagiert
displayC
clickedDigitS updatedEnteredNumberS
3 3
27. In FRP als Diagramm – Schritt 2
„Nächster Strom bekommt die 3, holt sich den aktuellen Zustand
aus Display, verknüpft beide über eine Berechnung
displayC
clickedDigitS updatedEnteredNumberS
3
0
0*10 + 3
3
28. In FRP als Diagramm – Schritt 3
„Ende der Transaktion, Ergebnis wird in Zelle gespeichert
„Externe Listener werden benachrichtigt
displayC
clickedDigitS updatedEnteredNumberS
3
30. In Gänze mit Schleife
„displayC = new CellLoop<>();
updatedEnteredNumberS = ...; // vorherige Folie
displayC.loop(updatedEnteredNumberS.hold(0L));
displayC
clickedDigitS updatedEnteredNumberS
displayC ist
immer
definiert
31. Feature 2: Mehrfaches Plus
„Ist nur mit Display
nicht möglich
„IMHO: 2 Register
123 + 2 +
32. Mit zwei Register: Main und Back
123 + 2 +
m=0
b=0
m=123
b=0
m=0
b=123
m=2
b=123
m=0
b=125
33. In FRP als Diagramm
displayC
clickedDigitS updatedEnteredNumberS
clickedPlusS
mainC
backC
:CombinedState
updateStateFromPlusS
Das hier ist die
fachliche Komplexität
des halben Rechners
=>
Er ist tatsächlich nicht
so einfach.
34. Lift: Abgeleitete Zustände
„class CombinedState {
final Long display;
final Long back;
final Long main;
CombinedState(Long display,
Long main, Long back) {...}
}
„final Cell<CombinedState> calculatorStateC =
displayC.lift(mainC, backC,
CombinedState::new);
displayC
mainC
backC
:CombinedState
35. Die Berechnung bei Plus
„final Stream<CombinedState> updatedStateFromPlusS =
clickedPlusS.snapshot(
calculatorStateC, (unit, state) -> {
Long result = state.main + state.back;
return new CombinedState(result, 0L, result);
});
clickedPlusS
calculatorStateC:CombinedState
updateStateFromPlusS
38. Finale Ausbaustufe
„Mit
§zusätzlicher „-“ Operation
§ „=“ Taste für Abschluss von Berechnungen
„Code hier mal ausgelassen
(als Übung für den interessierten Zuhörer :-)
39. Vergleich der LOCs pro Ausbaustufe
„Gemessen wurde der Controller nicht die GUI (86 LOC)
„LOCs steigen „vernünftig“ mit der fachlichen Komplexität (Anstieg
zwischen „Addition“ und „Finale“ ist erstaunlich gering)
„Code lässt sich IMHO gut refactoren bei neuen Anforderungen
0
20
40
60
80
100
120
LOC
Anzeige Addition Finale
40. Testen lässt es sich auch
„@Test
void pressDigit123() {
calculatorController.pressDigit(1L);
calculatorController.pressDigit(2L);
calculatorController.pressDigit(3L);
assertEquals(Long.valueOf(123L), getDisplay());
}
„private Long getDisplay() {
return calculatorController
.getDisplayCell().sample();
}
42. ... Ich will Web!
„Sodium ist Framework-unabhängig.
„Hier der Taschenrechner mit
§Typescript + Sodium + Angular
https://blog.akquinet.de/2018/06/24/functional-reactive-
programming-with-angular-and-sodium/
(etwas komplexer, um Komponententauglichkeit zu evaluieren.)
54. Was der User sieht ....
Kein Flackern,
deterministischer
Zustand
55. Transaktionen in der GUI - Kommentare
„Start von Transaktionen
§Automatisch, wenn extern ein Ereignis erzeugt wird
§Oder händisch über Transaction Bibliotheksklasse
„Zusammenfassung über Events durch:
// Kombinationsfunktion, falls beide Streams einen Event anbieten
s1.merge(s2, (l,r) -> l)
// Kurzschreibweise für oben
s1.orElse(s2)
57. Geschenke durch Funktionale Programmierung
„Referenzielle Transparenz reduziert Threading-
Probleme
„FRP
§analysiert Abhängigkeiten
§arbeitet diese sequenziell ab
§und hat daher keine Nebenläufigkeitsprobleme
58. Herausforderungen der Nicht-FP-Welt
„Einstieg in FRP über send()
void pressDigit(Long digit) {
clickedDigitS.send(digit);
}
Aus welchen Threads ist der Aufruf erlaubt?
„Ausstieg aus FRP über Listener
Operational
.updates(displayC)
.listen(displayListener);
Was dürfen die Listener und in welchem Thread laufen sie?
59. Die Regeln
„Sodiums Threading-Eigenschaften
§ send() kann aus (fast) jedem Kontext ausgeführt werden, ohne zu
blockieren
§ Ausführung der Listeners auf dem Thread, der send() ausgeführt
hat
„Regeln von Sodium für (externe) Listeners
§ Kein Aufruf von send()
§ Keine blockierende Operation (muss an anderen Thread delegiert
werden)
„Zusammengefasst: Externer Code kümmert sich um Threading (z.B. Mit
klassichen Workern)
61. RX ist überall
„Kontinuierliche Weiterentwicklung
„Z.B. direkt in Angular eingebaut.
„Aus unserer Erfahrung häufig verwirrend, z.B.
§ Hot (Event-Weiterleitung)
vs.
Cold (Kapselung eines Stream-Erzeugers)
§ Life-Cycle-Callbacks (next(), error(), complete())
§ Menge an Funktionalitäten
RX-Subject (~ 125 Methoden)
vs.
Sodium-Stream (~25 Methoden)
62. RxJs und FRP
„Keine klare Trennung zwischen der „reinen“ FRP Welt und dem Rest
des Codes
=> keine FRP-Kompositionseigenschaften
(man verliert leicht schöne Eigenschaften, s. die 6 Plagen)
„RxJs bietet viele unterschiedliche Dinge, insb. I/O Unterstützung,
Threading aber auch Rechenfunktionalität (Listenverarbeitung über
Cold-Observables)
„RxJs hat kein Transaktionskonzept und keine erzwungene
Zusammenführung für gleichzeitige Ereignisse
„Mit einem Subset von Rx lässt sich FRP-ähnlich entwickeln. (Stream =
Hot Observable, BehaviorSubject = Cell)
66. Persönliche Bewertung
„Nach
§ Einigen explorativen Fingerübungen (JavaFX, Angular/TS)
§ Einem Swing-Portal für eine Mikroservicearchitektur
„Gefühl
§ Ausgereiftes, abgeschlossenes Framework
=> kein aufgetretenes Problem lag an Sodium
§ Einarbeitung benötigte kurzes Umdenken, dann war es einfach.
(Vorbedingung: Grundlagen funktionaler Programmierung)
§ Trennung von konkretem GUI-Framework fühlte sich gut an.
Dennoch: Eine Integration könnte den Komfort erhöhen.
68. Die letzte Folie
„Functional Reactive Programming
ist ein stringentes Programmiermodell
für GUIs
mit schönen Eigenschaften
„Sodium
hat bis jetzt einen stabilen und vollständigen Eindruck und Spaß
gemacht.
„Listeners sind out.
„Und, ja, wir sind auch auf der Suche nach neuen Kollegen :-)