Um den Buchclub zu Funktionale Programmierung in Java geht es in der einhundertachtundfünfzigsten Episode des IT-Berufe-Podcasts.
TL;DR
Ich empfehle dir "Funktionale Programmierung in Java" von Herbert Prähofer als Lektüre ab dem zweiten Ausbildungsjahr. Das Buch vermittelt funktionale Grundlagen mit ausführlichen Codebeispielen und behandelt auch asynchronen Code sowie eigenschaftsbasierte Tests. Für deine Praxis und mögliche Prüfungsfragen sind besonders reine Funktionen, Funktionen höherer Ordnung und unveränderbare Daten interessant. Mit map und flatMap kannst du Daten verarbeiten und Funktionsaufrufe verketten, während CompletableFuture dir hilft, mit später verfügbaren Ergebnissen zu arbeiten.
Podcast: Play in new window | Download (Duration: 1:20:33 — 36.9MB)
Abonnieren: Apple Podcasts | Spotify | RSS
Kapitelmarken
- [00:00:00] Buchclub: Funktionale Programmierung in Java
- [00:01:53] Funktionale Konzepte für IT-Azubis und Prüfungen
- [00:05:09] Stärken des Buchs und praxisnahe Beispiele
- [00:08:38] Java-Schwerpunkt und anspruchsvolle Beispiele
- [00:11:41] Buchkapitel: Einstieg und sprachliche Grundlagen
- [00:13:08] Seiteneffekte und Funktionsparameter
- [00:14:28] Funktionen kombinieren und funktionale Datenstrukturen
- [00:15:47] Streams und parallele Verarbeitung
- [00:17:11] Asynchronität und Reactive Streams im Buch
- [00:18:30] Eigenschaftsbasierte Tests und Shrinking
- [00:20:54] Weiterführende Konzepte und Leseempfehlung
- [00:22:04] Funktionale Grundlagen für mögliche Prüfungsfragen
- [00:23:06] Lambda-Ausdrücke als anonyme Funktionen
- [00:25:00] Typ-Inferenz und statische Typisierung
- [00:27:41] Algebraische Datentypen: Produkt- und Summentypen
- [00:32:13] Pattern-Matching
- [00:34:47] Generics und parametrisierbare Klassen
- [00:37:34] Ko- und Kontravarianz
- [00:42:15] Begrenzte Wildcards in Java
- [00:43:25] Grundprinzipien der funktionalen Programmierung
- [00:44:01] Funktionen als First Class Citizens
- [00:45:22] Reine Funktionen und Seiteneffekte
- [00:47:52] Funktionen höherer Ordnung
- [00:50:43] Bedarfsauswertung und unendliche Folgen
- [00:53:18] Unveränderbare Datenstrukturen
- [00:55:40] Rekursion und Tail Call Optimization
- [00:58:21] Referenzielle Transparenz
- [00:59:59] Funktoren und map
- [01:01:24] Monoide, fold und neutrale Elemente
- [01:04:52] Monaden und flatMap
- [01:08:46] Funktionsketten und Streams im Alltag
- [01:11:18] Asynchrone Abläufe und reagierende Anwendungen
- [01:13:09] CompletableFuture und später verfügbare Ergebnisse
- [01:14:32] Optional und Future über Funktionen verarbeiten
- [01:16:48] Rückblick und Einordnung für die Prüfung
- [01:19:16] Ergänzende Links und Feedback
Inhalt
Die folgenden Inhalte werden im Buch besprochen. Meiner Meinung nach sollte jeder Azubi zum Fachinformatiker Anwendungsentwicklung diese Begriffe wenigstens einmal gehört haben, besser noch sie mit eigenen Worten erklären können. Die funktionale Programmierung ist neben der Objektorientierung heutzutage sicherlich eine gängige Vorgehensweise.
Für die anderen IT-Berufe sind viele Inhalte sicherlich etwas zu speziell. Allerdings sollte jeder ITler zumindest die verbreiteten Programmierparadigmen auseinanderhalten können. Und dazu zählen meiner Meinung nach auch einige wichtige Schlagworte. So wie bei der Objektorientierung Vererbung und Polymorphie erklärt werden müssen, sollte man bei der funktionalen Programmierung Funktionen höherer Ordnung oder reine Funktionen erläutern können.
Grundlagen
- Lambda-Ausdrücke: Anonyme Funktionen in einer kurzen Schreibweise, meist mit einem „Pfeil“ als Trenner zwischen Methodenkopf und -rumpf (z.B.
->). - Typinferenz: Implizite Ableitung der Datentypen z.B. von Funktionsparametern aus dem Kontext, z.B.
var text = "Text"(textist „automatisch“ einString). - Algebraische Datentypen
- Produkttypen: Der Datentyp wird definiert durch die Kombination verschiedener Werte. Das sind letztlich unsere bekannten Objekte bzw. Structs.
- Summentypen: Der Datentyp wird definiert als Liste möglicher Alternativen bzw. Varianten, z.B. ist eine
Krankenversicherungentweder eineGesetzlicheKrankenversicherungoder einePrivateKrankenversicherung. Hierbei müssen die Alternativen nicht in einer Vererbungsbeziehung stehen, wie man am Beispiel vermuten könnte.
- Pattern Matching: Kontrollstruktur, die Datentypen „zerlegen“ kann. Quasi
switch„on steroids“. - Generics: Datentypen, die mit anderen Datentypen parametriert werden können. Sie bieten allgemeingültige Algorithmen an, die auf mehreren Datentypen ausgeführt werden können.
- Ko- und Kontravarianz: Legen fest, welche Verallgemeinerungen bzw. Spezialisierungen bei Typparametern von generischen Klassen erlaubt sind. Beispiele:
- Kovarianz:
List<Person> students = new List<Student>(); - Kontravarianz:
List<Student> students = new List<Person>();
- Kovarianz:
Prinzipien der funktionalen Programmierung
- Funktionsobjekte (first class citizens): Funktionen sind fester Bestandteil der Sprache und können z.B. Variablen zugewiesen oder als Parameter übergeben werden.
- Reine Funktionen (pure functions): Funktionen haben keine Seiteneffekte. Sie liefern zum gleichen Input immer den gleichen Output.
- Funktionen höherer Ordnung (higher order functions): Funktionen, die andere Funktionen zurückgeben oder als Parameter bekommen.
- Bedarfsauswertung (lazy evaluation): Ein Ausdruck wird erst dann ausgewertet, wenn sein Wert auch wirklich benötigt wird.
- Unveränderbarkeit (immutability): Bestehende Werte sollen nicht mehr verändert werden. Stattdessen werden immer neue Werte erzeugt.
- Rekursion (recursion): Das Mittel der Wahl zur Iteration ohne veränderliche Variablen.
- Referentielle Transparenz (referential transparency): Ein Ausdruck kann jederzeit durch seinen Wert ersetzt werden und umgekehrt.
Funktionale Datenstrukturen
- Funktoren: Bieten die Funktion
map()an, um den enthaltenen Datentypen in einen anderen zu transformieren. Beispiel:Optional<T>. - Monoide: Mit Monoiden kann man eine Menge von Elementen zu einem Gesamtergebnis verknüpfen, es also reduzieren (fold). Dafür wird ein neutrales Element und eine Reduktionsfunktion benötigt. Beispiel: Addition ganzer Zahlen mit neutralem Element
0. - Monaden: Bieten die Funktionen
of()undflatMap()an, um den enthaltenen Datentypen in einen anderen zu transformieren, wobei das Ergebnis dieser Transformation selbst wieder eine Monade ist. Beispiel:Stream<T>. - Listenverarbeitung mit
Streams- Grundlagen von
map(),filter()undreduce. - Parallele Streams und Eignung von Aufgaben für die Parallelisierung
- Grundlagen von
Sonstiges
- Asynchronität mit
CompletableFutures und Reactive Streams - Property Based Testing mit Quickcheck
- Ein komplett anderer Ansatz, um Funktionen zu testen. Statt Input und erwarteten Output selbst zu definieren, werden zufällige Input-Werte generiert und allgemeine Eigenschaften des Ergebnisses geprüft, z.B. dass bei einer Konkatenation zweiter Strings die Länge des Ergebnisses immer größer oder gleich der Länge des ersten Strings sein muss.
Was mir am Buch sehr gut gefällt
- Es werden nicht nur die Grundlagen der funktionalen Programmierung erläutert, sondern auch viele andere wichtige Begriffe rund um die moderne Programmierung.
- Gerade die Grundlagen werden sehr verständlich erklärt und mit vielen praxisnahen Beispielen angereichert.
- Es werden auch sehr moderne Konzepte wie wie Reactive Streams und Property Based Testing erläutert.
- Einige Grundlagen, die jeder Informatiker einmal gehört haben sollte, werden ebenfalls behandelt, z.B. Algorithmen zur Tiefensuche.
- Auch einige Entwurfsmuster werden im Detail erklärt und mit der funktionalen Lösung verglichen.
- Die technischen Hintergründe der Implementierung in Java werden gut verständlich erläutert. Das sollte heutzutage jeder Java-Entwickler kennen!
Was mir am Buch nicht ganz so gut gefällt
- Die Inhalte haben natürlich einen starken Fokus auf die Implementierung in Java. Man kann zwar auch als Nicht-Java-Entwickler im Bereich der Grundlagen einiges mitnehmen, aber am besten geeignet ist das Buch wohl für Hardcore-Java-Entwickler.
- Man merkt, dass der Autor Hochschuldozent ist. Die Beschreibungen und Beispiele sind teilweise etwas akademisch. Auch wenn die funktionale Programmierung ihren Ursprung in der Mathematik hat, ist das vielleicht für Azubis manchmal etwas schwere Kost.
- Die verwendeten Beispiele sind teilweise sehr umfangreich. Sehr lange Codeausschnitte, die schwer nachzuvollziehen sind, hätten vielleicht gekürzt werden können.
Fazit
- Ich kann das Buch uneingeschränkt weiterempfehlen. Es vermittelt einen sehr guten Überblick über moderne Programmmiertechniken und die funktionale Programmierung.
- Auch Azubis werden es gut verstehen können. Ich empfehle die Literatur allerdings erst ab dem zweiten Lehrjahr, wenn schon einige Programmierprojekte umgesetzt wurden.
- Auch als Nachschlagewerk für zentrale Begriff der funktionalen Programmierung finde ich das Buch sehr gut, da die Begriffe kurz und prägnant definiert und mit Beispielen erklärt werden.
- Insbesondere für Java-Entwickler empfehle ich das Buch zum Verständnis der technischen Implementierung der funktionalen Programmierung in Java.
Literaturempfehlungen
Naja, was soll ich wohl anderes empfehlen als das besprochene Buch Funktionale Programmierung in Java*? 🙂
Links
- Permalink zu dieser Podcast-Episode
- RSS-Feed des Podcasts
- Gregor Trefs über funktionale Programmierung, Konferenzen und Fachartikel
- Algebraic data type
- Summentypen
- Produkttypen
- Property-based Testing mit JUnit QuickCheck
- Präsentation: Property-based Testing mit JUnit-Quickcheck
- Railway Oriented Programming in Java
Zusammenfassung
Das Buch für deine Ausbildung
Mit meinen IT-Azubis lese ich regelmäßig Fachbücher. "Funktionale Programmierung in Java" von Herbert Prähofer, Professor an der Universität Linz, empfehle ich dir als deutschsprachigen Einstieg in funktionale Konzepte und ihre Umsetzung in Java. Meine Besprechung stammt aus dem Jahr 2020.
Durch die ausführlichen Codebeispiele kannst du nachvollziehen, wie die Konzepte im Alltag funktionieren. Das Buch erläutert auch technische Hintergründe und Entscheidungen zur Sprache Java. Ergänzend behandelt es beispielsweise die Tiefensuche in Graphen oder Bäumen sowie Entwurfsmuster.
Für dich als IT-Azubi passt die Lektüre besonders ab dem zweiten Ausbildungsjahr, wenn du bereits Erfahrung beim Programmieren gesammelt hast. Im dritten Jahr kannst du die Inhalte auf dein Abschlussprojekt beziehen. Einige Beispiele sind akademisch geprägt und erstrecken sich über mehrere Seiten, wodurch du beim Lesen längere Abläufe im Kopf verfolgen musst. Den Beispielcode kannst du herunterladen und selbst ausprobieren.
Der Schwerpunkt liegt auf Java, wobei ein großer Teil der Konzepte auch auf andere Sprachen übertragbar ist. Wenn du etwa mit C# arbeitest, findest du ähnliche funktionale Möglichkeiten. Die Einführungskapitel eignen sich außerdem zum Nachschlagen einzelner Begriffe.
Funktionale Konzepte in Ausbildung und Prüfung
Seit der Neuordnung der IT-Berufe 2020 gehört das Unterscheiden von Programmiersprachen zur gemeinsamen Grundausbildung. Damit sind Programmierparadigmen für dich auch dann interessant, wenn du einen anderen IT-Beruf als Fachinformatiker:in Anwendungsentwicklung lernst. Java und C# enthalten funktionale Konzepte, die sich mit objektorientiertem Code kombinieren lassen.
Als Prüfer frage ich bei einem Java-Abschlussprojekt nach, wie eingesetzte Streams oder Optional funktionieren und weshalb du sie auf eine bestimmte Weise verwendest. Von dir als Fachinformatiker:in Anwendungsentwicklung erwarte ich, dass du grundlegende funktionale Begriffe wie reine Funktionen und Funktionen höherer Ordnung erklären kannst. Auch Generics und Entwurfsmuster gehören zu den angesprochenen Prüfungsthemen.
Funktoren, Monoide und Monaden dienen vor allem dazu, die Hintergründe besser zu verstehen. Ihre ausführliche mathematische Behandlung geht für eine mündliche Prüfung aus meiner Sicht weit.
Was die zwölf Buchkapitel behandeln
- Einleitung: Grundbegriffe der funktionalen Programmierung und erste Hinweise zur Umsetzung in Java.
- Sprachliche Grundlagen: Generics einschließlich Ko- und Kontravarianz, Typ-Inferenz sowie Typ-Constraints. Dazu kommen Neuerungen aus Java 8, etwa Default-Methoden in Interfaces, und die Grundlagen von Lambda-Ausdrücken, Closures sowie funktionalen Interfaces.
- Programmieren ohne Seiteneffekte: Reine Funktionen und referenzielle Transparenz. Das Kapitel behandelt außerdem Exceptions und funktionale Datenstrukturen, darunter Listen und Tupel.
- Programmieren mit Funktionsparametern: Funktionen höherer Ordnung anhand konkreter Beispiele, etwa der Tiefensuche. Auch die Entwurfsmuster Strategie, Kommando und Besucher werden mit funktionalen Ansätzen behandelt. Weitere Themen sind Pattern-Matching und Bedarfsauswertung.
- Funktionen kombinieren: Fluent Interfaces und Aufrufketten mit Java-Beispielen rund um
ComparatorundCollector. - Funktoren, Monoide und Monaden: Die Strukturen werden über ihre Methoden und anhand von Beispielen erklärt.
Optionalund Streams zeigen, wie du damit in Java arbeitest. - Streams: Elemente mit
mapumwandeln, mitfilterauswählen und mitreducezu einem Ergebnis zusammenführen. - Parallele Streams: Voraussetzungen für parallel bearbeitbare Probleme und der Aufwand, den paralleler Code zusätzlich verursacht. Tests im Anhang untersuchen, wie sich das auf die Laufzeit auswirkt.
- Asynchronität: Mit
CompletableFutureErgebnisse verarbeiten, die erst später verfügbar sind. - Reactive Streams: Asynchrone Ergebnisse und Ereignisse mit RxJava verarbeiten. Das Kapitel beschreibt die beteiligten Klassen sowie passende Einsatzfälle.
- Testen: Möglichkeiten von JUnit 5 und AssertJ sowie eigenschaftsbasierte Tests nach dem QuickCheck-Verfahren.
- Weiterführende Konzepte: Ergänzende Möglichkeiten aus anderen Sprachen, deren Übernahme in Java zum Zeitpunkt der Besprechung teilweise noch diskutiert wurde.
Sprachgrundlagen verständlich erklärt
Lambda-Ausdrücke und Typ-Inferenz
Wenn du eine kurze Funktion direkt an der Stelle brauchst, an der du sie übergibst, kannst du einen Lambda-Ausdruck schreiben. Dabei handelt es sich um eine anonyme Funktion. Ein Beispiel ist (a, b) -> a + b: Die Funktion bekommt zwei Werte und liefert ihre Summe. Je nach Syntax lassen sich Angaben zum Typ oder ein ausdrücklich geschriebenes return einsparen.
Damit der Compiler die Typen trotzdem kennt, leitet er sie aus dem Kontext ab. Das heißt Typ-Inferenz. Bei einer lokalen Variablen wie var text = "Hallo" ergibt sich der Typ aus dem zugewiesenen Wert. Die Variable bleibt statisch typisiert und hat damit einen festgelegten Datentyp.
Algebraische Datentypen
Beim Modellieren eigener Daten kannst du verschiedene Typen kombinieren oder mögliche Varianten beschreiben:
- Ein Produkttyp verbindet mehrere Bestandteile. Ein Auto mit einer Marke als String und einem Kilometerstand als Integer ist ein solches Beispiel. Vergleichbare Strukturen kennst du als Objekte, Structs oder Records.
- Ein Summentyp beschreibt alternative Varianten eines Datentyps. Eine Krankenversicherung kann beispielsweise als gesetzliche oder private Krankenversicherung auftreten. Die beteiligten Klassen können dabei unabhängig voneinander definiert sein.
Im Buch dienen auch boolesche Ausdrücke als Beispiel: Sowohl das Literal true als auch ein Vergleich wie a > 5 können als Varianten eines gemeinsamen Ausdruckstyps betrachtet werden.
Pattern-Matching
Wenn du mit verschiedenen Varianten eines Datentyps arbeitest, kannst du über Pattern-Matching den passenden Code auswählen. Dabei lassen sich auch Werte innerhalb eines Objekts prüfen.
Beim Auto könnte etwa dessen Höhe bestimmen, welcher Fall ausgeführt wird. Pattern-Matching kann das Objekt in seine Bestandteile zerlegen und einzelne Werte an Variablen binden, mit denen du anschließend weiterarbeitest.
Generics und Varianz
Eine Liste soll unabhängig davon funktionieren, welchen Elementtyp du darin speicherst. Über Generics definierst du die Klasse einmal mit einem Platzhalter wie T. Bei einer konkreten Liste legst du den Typ fest, beispielsweise durch List<Integer>. Der Compiler prüft dann die Typen der eingefügten und zurückgegebenen Elemente.
Mit Ko- und Kontravarianz beschreibst du, wie sich Beziehungen zwischen den Elementtypen auf die parametrisierten Typen übertragen:
- Kovarianz folgt der Richtung der Typbeziehung.
- Kontravarianz kehrt diese Richtung um.
In Java sind List<Student> und List<Person> zunächst eigenständige Typen, auch wenn Student von Person erbt. Würdest du eine Liste von Studierenden als beliebig beschreibbare Personenliste behandeln, könntest du darüber beispielsweise eine:n Professor:in hinzufügen. Die Liste muss jedoch ihren festgelegten Elementtyp behalten.
Java bietet dafür begrenzte Wildcards. List<? extends Person> beschreibt eine Liste mit Person oder einem davon abgeleiteten Typ als Elementtyp. Bei ? super Person bezieht sich die Grenze auf Person und übergeordnete Typen.
Grundprinzipien der funktionalen Programmierung
Funktionen als Werte
Mit Functions as First Class Citizens sind Funktionen ein Bestandteil der Sprache, mit dem du wie mit anderen Werten arbeiten kannst. Du kannst sie einer Variablen zuordnen oder als Parameter übergeben. In Java ermöglichen Lambda-Ausdrücke und funktionale Interfaces solche Arbeitsweisen.
Reine Funktionen
Wenn du einer reinen Funktion dieselben Parameter übergibst, erhältst du stets dasselbe Ergebnis. Sie arbeitet ohne Seiteneffekte. Bei einer Addition von 3 und 5 kommt dadurch jedes Mal 8 heraus.
Ein interner Zähler, der das Ergebnis beeinflusst und sich bei jedem Aufruf verändert, macht das Verhalten vom Zustand abhängig. Auch eine Ausgabe auf die Konsole ist ein Seiteneffekt. Reine Funktionen lassen sich leichter testen, weil du ihre Eingaben vollständig festlegen kannst.
Funktionen höherer Ordnung
Eine Funktion höherer Ordnung bekommt eine andere Funktion als Parameter oder gibt eine Funktion zurück. Bei map übergibst du beispielsweise eine Funktion, die aus einer Person deren Vornamen ermittelt. Für jedes Element wird diese Funktion angewendet, sodass du anschließend die Namen erhältst.
Den Ablauf zum Verarbeiten der Elemente kannst du dabei wiederverwenden. Welche Werte entstehen, bestimmst du über die übergebene Funktion.
Bedarfsauswertung
Bei Lazy Evaluation wird ein Ausdruck erst ausgewertet, sobald sein Wert gebraucht wird. Das spart Arbeit bei Ergebnissen, auf die dein Programm später gar nicht zugreift.
Damit lassen sich auch unendliche Folgen beschreiben. Ein Stream erzeugt jeweils die angeforderten Elemente. Wenn du genug Werte erhalten hast, endet die Verarbeitung an dieser Stelle.
Unveränderbare Daten
Bei Immutability behalten vorhandene Werte ihren Inhalt. Wenn du einer unveränderbaren Liste ein Element hinzufügst, entsteht eine neue Liste, während die bisherige erhalten bleibt.
Das erleichtert den Umgang mit parallelem Code, weil Threads auf Daten arbeiten können, deren Inhalt stabil bleibt. Funktionale Datenstrukturen können Teile gemeinsam verwenden, wodurch für eine neue Liste weniger kopiert werden muss. Für Java gibt es Bibliotheken mit solchen Datenstrukturen.
Rekursion
Wenn eine Funktion sich selbst aufruft, arbeitest du rekursiv. Dabei erhält jeder Aufruf seine eigenen Parameter. In funktionalen Ansätzen ist das ein Mittel, Abläufe zu wiederholen und mit unveränderbaren Werten zu arbeiten.
Mit Tail Call Optimization kann eine Sprache einen rekursiven Aufruf am Ende einer Funktion in einen iterativen Ablauf überführen. Java führt diese Optimierung bei solchen Aufrufen nicht automatisch durch. Bei tiefen Rekursionen kann deshalb der Stack voll werden.
Referenzielle Transparenz
Wenn du einen Ausdruck durch seinen Wert ersetzen kannst und das Verhalten des Programms gleich bleibt, ist er referenziell transparent. Bei einer reinen Additionsfunktion sind addiere(3, 5) und 8 gleichwertig. Dadurch kannst du leichter nachvollziehen, welches Ergebnis ein Ausdruck liefert.
Funktionale Datenstrukturen im Alltag
Funktoren und map
Über map kannst du enthaltene Werte umwandeln. Eine Struktur mit dieser Möglichkeit wird hier als Funktor beschrieben. Bei einem Stream aus Personen kann daraus ein Stream mit ihren Vornamen werden.
Auch Optional bietet map an. Enthält es einen Wert, wird die Funktion darauf angewendet. Ein leeres Optional bleibt leer.
Monoide und fold
Beim Zusammenführen mehrerer Werte zu einem Gesamtergebnis spielen Monoide eine Rolle. Mit fold verknüpfst du den bisherigen Zwischenwert jeweils mit dem nächsten Element.
Dazu brauchst du einen passenden Startwert, das neutrale Element der Operation. Für eine Summe ist das 0; bei einer Multiplikation ist es 1. So verändert der Startwert das Ergebnis nicht. Für eine Summe beginnst du beispielsweise mit 0 und addierst anschließend jeden weiteren Wert.
Monaden und flatMap
Bei Monaden stehen zwei Möglichkeiten im Mittelpunkt: Du kannst einen Wert in die Struktur einbetten und anschließend mit flatMap weiterarbeiten. Beispielsweise macht Optional.of aus einem String ein Optional<String>.
An flatMap übergibst du eine Funktion, die selbst wieder eine entsprechende Struktur liefert. Damit lässt sich eine zusätzliche Verschachtelung vermeiden.
Wenn jede Person mehrere Kurse belegt, ergibt das Abbilden der Personen auf ihre Kurslisten zunächst eine Liste von Listen. Mit flatMap kannst du die enthaltenen Kurse in einer gemeinsamen Folge zusammenführen. Weitere Aufrufe lassen sich anschließen, etwa um zu jedem Kurs dessen Namen zu erhalten.
Bei solchen Abläufen übernimmt die API das Durchlaufen der Elemente. In meinen Code-Reviews mit IT-Azubis prüfe ich deshalb, welche Schleifen sich mit Stream-Operationen ausdrücken lassen.
Asynchron mit späteren Ergebnissen arbeiten
Wenn ein Download länger dauert, sollst du währenddessen weiter mit der Anwendung arbeiten können. Bei einem asynchronen Ablauf läuft die Aufgabe im Hintergrund, während der aufrufende Code weitere Arbeit erledigt. Sobald das Ergebnis verfügbar ist, kann der dazugehörige nächste Schritt ausgeführt werden.
In Java beschreibt CompletableFuture ein Ergebnis, das später verfügbar wird. Ein Download kann beispielsweise ein CompletableFuture<File> zurückgeben. Über dieses Objekt legst du fest, was mit der Datei passieren soll, sobald sie vorliegt, etwa sie auf der Festplatte zu speichern.
Wenn du ein Ergebnis über einen wartenden Zugriff abfragst, blockiert dieser Zugriff bis zum Abschluss der Aufgabe. Durch verkettete Folgeschritte kannst du den Ablauf asynchron weiterführen.
Auch bei Optional kannst du die angebotenen Funktionen direkt einsetzen. Mit map beschreibst du, wie ein vorhandener Wert weiterverarbeitet werden soll. Die Struktur übernimmt den Umgang mit dem vorhandenen oder leeren Inhalt.
Für Web-Anwendungen mit asynchronen Requests sind solche Abläufe ebenfalls relevant. Das Buch vertieft sie mit CompletableFuture und Reactive Streams auf Basis von RxJava.
Eigenschaftsbasierte Tests
Bei Property-Based-Testing erzeugt ein Framework zufällige Eingabewerte. Du beschreibst eine Eigenschaft, die das Ergebnis für gültige Eingaben erfüllen soll. Das Framework prüft diese Eigenschaft anhand der erzeugten Fälle.
Schlägt ein Test fehl, kann Shrinking die Eingabe schrittweise verkleinern, bis ein kleinerer Fall übrig bleibt, der den Fehler weiterhin auslöst. Dadurch erkennst du leichter, welche Eingabe den Fehler verursacht. Das Buch behandelt diesen Ansatz neben JUnit 5 und AssertJ.
Ergänzende Materialien
Meine Vorträge und Artikel zur funktionalen Programmierung sind in den Shownotes unter itberufepodcast.de/158 verlinkt. In Episode 106 findest du außerdem ein Interview zur funktionalen Programmierung.
Transkription der gesamten Episode
Automatisch erzeugte Transkription der Episode
Herzlich willkommen zum IT-Berufe-Podcast, dem Podcast rund um die Ausbildung in den
IT-Berufen. In dieser Episode gibt es einen Buchclub zum Buch Funktionale Programmierung
in Java. Viel Spaß! Hallo und herzlich willkommen zur 158. Episode des IT-Berufe-Podcasts. Mein
Name ist Stefan Macke und heute mache ich mal wieder eine Buchclub-Episode. Wer das nicht
kennt, ich lese mit meinen Azubis regelmäßig Bücher rund um die Ausbildung in der IT und einige
davon bespreche ich jetzt auch hier oder habe ich auch schon hier besprochen, zum Beispiel das ganze
IT-Handbuch von Sascha Kersten. Das kennst du bestimmt, dein sinnvoller Ausbildungsbegleiter.
Aber ich habe auch noch mehr Bücher, zum Beispiel JUnit mit Java 8 und verschiedene andere Sachen
gemacht. Und jetzt habe ich ein neues Buch gefunden, was ich richtig, richtig gut finde
als Einstieg in die Funktionale Programmierung. Und das heißt Funktionale Programmierung in Java.
Wenig überraschend in dieser Titel. Das ganze wurde geschrieben von einem Professor an der
Universität Linz in Österreich und der Name ist Herbert Prähofer. Ist tatsächlich auch mal ein
original deutsches Buch, also kein englisches, was dann übersetzt wurde oder sonst irgendwas,
sondern wirklich mal ein gutes deutsches Buch um die Programmierung, also rund um die Programmierung.
Das ist erwähnenswert, so viele gibt es davon nicht. Und ich habe das Buch tatsächlich verschlungen,
weil da so viele coole Sachen drin sind. Also eine ganz allgemeine Einführung in die Funktionale
Programmierung, aber auch Spezifika, wie das ganze in Java umgesetzt ist und auch noch viele
weitergehende Themen. Nicht nur die Funktionale Programmierung, sondern auch moderne Sachen, wie
zum Beispiel Reactive Streams. Und von daher kann ich das Buch Stand heute 2020 auf jeden Fall
uneingeschränkt weiterempfehlen und an den Grundlagen wird sich auch so schnell nicht viel
ändern. Also ich denke mal auch in den nächsten Jahren kann man das noch gut als Einstieg in die
Funktionale Programmierung lesen in der Ausbildung. So, und jetzt könnte man meinen,
Funktionale Programmierung, das ist nur was für Anwendungsentwickler. Ja, im Detail natürlich
schon. Im täglichen Anwendungsbereich bei der Programmierung natürlich sollte man als
Anwendungsentwickler vielleicht sich inzwischen damit mal beschäftigt haben, weil die Objektorientierung
ja nicht mehr nur das Einzige ist, was heute in den Sprachen angeboten wird und auch nicht das
Einzige Sinnvolle. Es gibt viele gute Einsatzszenarien, wo ich funktionale Paradigmen
einsetzen kann. Aber davon abgesehen ist trotzdem die Funktionale Programmierung auch für alle
anderen IT-Berufe interessant. Denn gerade ab 2020, Novellierung der IT-Ausbildungsordnung,
da steht auch für die anderen IT-Berufe sowas wie Unterscheiden verschiedener Programmiersprachen
drin. Und zwar sogar in der Grundausbildung, die für alle IT-Berufe identisch ist. Und da
sollte man ja wenigstens mal, wenn man schon nicht Programmiersprachen konkret unterscheiden kann,
was in der Prüfung wohl nie abgefragt wird, dann eher die Programmierparadigmen abgrenzen können.
Und Objektorientierung, das macht vermutlich jeder Azubi in der Berufsschule und vielleicht auch in
seinem Unternehmen, weil es ja immer noch sehr dominant ist. Aber Funktionalprogrammierung gehört
für mich heutzutage auch dazu. Unter anderem, weil viele objektorientierte Sprachen ja auch
funktionale Aspekte inzwischen eingebaut haben. Siehe C-Sharp oder eben auch Java oder andere
Sprachen wie Ruby. Die haben alle schon teilweise seit Jahren solche funktionalen Konzepte mit
eingebaut, sodass man Objektorientierung und Funktionalprogrammierung ja so ein bisschen
mischen kann und sich das Beste aus beiden Welten so ein bisschen rausholen kann. Dazu sollte ich
aber vielleicht wissen, was ist denn eigentlich das Beste aus beiden Welten und insbesondere das
Beste an der Funktionalprogrammierung. Und dafür empfehle ich ab jetzt dieses Buch, weil er wirklich
sehr gut erklärt wird, auch von, also from scratch quasi, von den Grundlagen, was es damit so auf sich
hat und was man damit schönes machen kann. Und wenn du persönlich Softwareentwickler bist und
vielleicht nicht unbedingt gerade für die Prüfung lernst, aber immer schon mal wissen wolltest, wie
so ein Stream in Java oder ein Optional oder sonstiges, was aus dem Bereich der Funktionalprogrammierung
kommt, eigentlich funktioniert, dann kann ich das Buch nur wärmstens empfehlen. Denn gerade für
Java-Entwickler ist da richtig, richtig viel drin, weil die technischen Hintergründe der Implementierung
in Java wirklich im Detail erläutert werden, sodass man also gut nachvollziehen kann, warum auch die
Sprachdesigner sich für bestimmte Sachen entschieden haben und das einfach auch versteht, wie der Code
funktioniert, den man da jetzt mit diesen funktionalen Möglichkeiten da anreichert. Also
meiner Meinung nach auch ein tolles Buch für fertige Entwickler, die sich immer schon mal mit
den Hintergründen dessen, was sie da eigentlich den ganzen Tag machen, beschäftigen wollen. Und wenn wir
dann noch mal zurück auf die Prüfungsinhalte kommen, also so wie ich bei der Objektorientierung
erwarten würde, dass ein Prüfling zumindest im Bereich der Anwendungsentwicklung so etwas wie
Vererbung, Polymorphie, Kapselung erklären kann, die klassischen drei Säulen der Objektorientierung, würde
ich heutzutage auch erwarten, dass man das gleiche auf die funktionale Programmierung überträgt und dann so
etwas wie Funktionen höherer Ordnung oder reine Funktionen erklären kann, weil das einfach wichtige
Bestandteile dieses Programmierparadigmas sind. Und Spoiler, die ganzen Sachen werden im Buch rauf
und runter erklärt und richtig schön mit anschaulichen Beispielen und nachvollziehbar, auch für Azubis,
wie ich finde. Richtig gut erklärt, deswegen meine absolute Empfehlung für dieses Buch. Und ich würde
jetzt diese heutige Episode zum Anlass nehmen, mal so ein bisschen über das Inhaltsverzeichnis zu
gehen. Was bietet das Buch eigentlich konkret für Inhalte? Und dann würde ich auch so ein bisschen
die Quintessenz des Ganzen hier mal erzählen, denn ich möchte ja auch, dass du das hier für
die Prüfungsvorbereitung nutzen kannst. Und anhand des Buchs erkläre ich heute also mal so ein
bisschen funktionale Programmierung und was es da so mit den wichtigsten Eigenschaften auf sich hat.
Doch bevor wir das durchgehen, vielleicht mal einmal im Detail, warum ich das Buch jetzt wirklich
gut finde und weiter empfehlen kann. Also es gibt einige Sachen, die ich sehr, sehr gut finde. Die
gehen wir mal kurz durch. Gibt auch ein paar Sachen, die ich vielleicht etwas optimieren würde. Und ja,
ich gehe mal meine Liste kurz durch. Also Nummer 1, das Buch redet zwar von funktionaler Programmierung.
Es steht im Titel, aber es werden auch sehr, sehr viele Sachen am Rande davon erklärt, die
einfach für den Alltag eines Entwicklers heute wichtig sind. Ich nehme jetzt nur das Beispiel
Reactive Streams. Ja, also richtig moderne Sachen für Praxiseinsatz. Was kann ich damit machen? Was
bringt mir das? Und so weiter. Also ich kann das im Jahr 2020 auch gerade für diesen heißen Scheiß
auf jeden Fall empfehlen, weil es da wirklich sehr, sehr gut verständlich und mit Beispielen
erklärt wird, wie das ganze Zeug funktioniert. Das ist auch mein weiterer nächster Punkt. Die
Beispiele sind richtig gut. Die sind teilweise auch sehr lang und gut verständlich und ja,
praxisnah gestaltet. Also das hat mir auch sehr gut gefallen. Nicht nur in der Theorie,
wie funktioniert das denn, sondern mit konkreten, nachvollziehbaren Code-Beispielen alles erklärt.
Was ich auch sehr gut fand, zusätzlich zu den Reactive Streams, war nochmal im Bereich Testen.
Das Property-Based-Testing hat auch viel mit funktionaler Programmierung zu tun,
wenn man das da gut einsetzen kann. Aber einfach mal eine ganz andere Art und Weise,
wie ich meine Tests schreibe zum Beispiel. Das fand ich auch sehr interessant, dass das hier im
Buch gleich mit abgehakt ist und mit drin ist. Und es werden aber auch nebenbei noch ganz andere
allgemeine Sachen besprochen, die nicht zwangsläufig was mit funktionaler Programmierung
zu tun haben, die aber jeder Informatiker meiner Meinung nach wenigstens mal gehört haben sollte.
Ich nehme mal ein Beispiel. Ein Algorithmus zur Tiefensuche in einem Grafen oder einem Baum. Wenn
du das noch nie gehört hast, ja, das Buch hilft dir weiter. Da wird quasi so nebenbei als Beispiel
für die Arbeit auf funktionalen Datenstrukturen sowas wie eine Tiefensuche erklärt. Das finde
ich richtig, richtig gut. Also es ist quasi auf jeder Seite ist eigentlich irgendwas drin,
was man mitnehmen kann für die Praxis. In die gleiche Bresche springen dann die
Entwurfsmuster, die auch im Buch erklärt werden. Einfach um zu zeigen, dass man in
funktionaler Programmierung vielleicht solche Dinge gar nicht mehr braucht. Bestimmte kann
man nämlich dort einfach durch eingebaute Funktionalität umsetzen. Und die werden hier
auch erklärt. Und wie ich sehe, es wird in den Prüfungen zumindest für Anwendungsentwickler
auch immer weiter in Richtung solcher Entwurfsmuster gehen. Ich habe die Handreichung
für die Neuordnung der IT-Berufe dieses Jahr gerade noch mal durchgeguckt und da steht auch
explizit irgendwo Design-Pattern, also Entwurfsmuster als Bestandteil der Ausbildung. Das heißt,
das muss man heute ja zumindest auch mal gehört haben, was ist das Ganze eigentlich und vielleicht
auch mal zwei, drei nennen können. Und die werden hier im Buch unter anderem auch mit erklärt. Also
auch eine schöne Geschichte für die Prüfung zum Beispiel. Und dann hatte ich eben schon gesagt,
die technischen Hintergründe der Implementierung in Java finde ich richtig klasse. Ja, ich kann
eigentlich nicht als Entwickler sagen, so ich benutze jetzt Java 8 und mache hier mal ein
Optional und bin ich funktionaler Programmierer, sondern ich müsste ja vielleicht auch mal
verstehen, was dahinter liegt, wie das Ganze implementiert ist, warum das vor allem wie
implementiert ist. Und das würde ich zum Beispiel auch in der Prüfung abfragen. Habe ich auch schon
in mehreren mündlichen Prüfungen gemacht. Wenn also ein Entwickler mit einem Java-Projekt um die
Ecke kommt und diese ganzen Sachen benutzt hat, Optional und Stream und irgendwas, und dann aber
nicht richtig benutzt hat, ja, also wie es eigentlich vorgesehen war, dann frage ich da
schon gerne mal nach. Und da offenbaren sich dann sehr schnell einfach auch Lücken von der
Erkenntnis, wie man mit diesen Strukturen eigentlich umgehen sollte und warum vor allem.
Und das sind so Sachen, die sind für mich in einem Java-Abschlussprojekt auf jeden Fall Teil
einer mündlichen Prüfung. Von daher muss man das nicht nur anwenden können, sondern auch verstehen
können. Und das wird mit diesem Buch sehr, sehr gut abgedeckt, weil alles im Detail erklärt wird.
So, ein paar Punkte habe ich auch, die mir nicht ganz so gut gefallen. Nummer eins ist gleich das,
was ich gerade erzählt habe. Es ist schon ein starker Fokus auf Java. Ja, also Leute,
die es nicht täglich mit Java entwickeln können, vermutlich mit der genauen Implementierung und den
Hintergründen in Java jetzt nicht so viel anfangen. Gefühlt sind trotzdem, ich sage einfach mal,
80 Prozent des Buchs unabhängig von Java, sind einfach nur die Konzepte. Aber natürlich,
wenn man jetzt nicht mit Java arbeitet, ja, muss man halt die technischen Details vielleicht ein
bisschen überspringen oder sie interessieren einen vielleicht nicht so viel. Na gut, aber das
Buch heißt ja auch in Java, von daher weiß man, worauf man sich einlässt. Aber ich finde schon,
dass die Konzepte im Großen und Ganzen allgemein erklärt werden. Und Java-Code lässt sich jetzt
auch relativ einfach auf andere Sprachen übertragen. Ich nehme jetzt mal C-Sharp als
Beispiel. Ist ja schon sehr ähnlich und die Konzepte sind dort auch sehr ähnlich umgesetzt.
Also von daher ist es nicht nur was für Hardcore-Java-Entwickler, aber wenn du einer
bist, wird das für dich auf jeden Fall noch mehr Mehrwert bringen, sage ich mal.
Dann merkt man stellenweise schon, dass der Autor ja Hochschuldozent ist. Ja,
also einige Beispiele sind schon etwas, ich sag mal, akademisch vorsichtig gesagt. Und auch,
wenn die funktionale Programmierung ja eigentlich auch aus der Mathematik kommt,
ist das vielleicht für Azubis, die gerade einsteigen, ein bisschen schwere Kost. Ja,
also einige Beispiele sind wirklich sehr, sehr lang und auch etwas schwieriger nachzuvollziehen,
deswegen. Von daher, wenn ich jetzt ein Azubi im ersten Layer bin, dann ist das vielleicht noch
nicht ganz die richtige Lektüre für mich, würde ich sagen. Aber ab dem zweiten Layer und vor allem
auch im dritten, wenn es aufs Abschlussprojekt zugeht und man die Sachen eigentlich in der
Praxis anwenden können sollte, da ist das Buch sehr, sehr gut geeignet. Und da glaube ich auch,
dass Azubis das verstehen können. Gerade zu Beginn der Ausbildung ist es vielleicht etwas
schwierig zu verstehen oder etwas schwieriger. Ja, und letzter Punkt habe ich gerade schon gesagt,
die Beispiele sind teilweise sehr umfangreich, also Codebeispiele, die über mehrere Seiten gehen. Da
muss man dann schon, wenn man das liest, im Kopf versuchen nachzuvollziehen, was passiert jetzt wie.
Das ist vielleicht doch etwas schwierig. Natürlich kann man sich den Code auch runterladen,
natürlich gibt es den als Anhang zum Buch, kann man sich einfach downloaden, entpacken und los
geht's. Aber wenn man jetzt nur das Buch liest, so wie ich es auch gemacht habe, dann ist das
teilweise doch etwas schwierig nachzuvollziehen. So, nichtsdestotrotz, wie gesagt, uneingeschränkte
Empfehlungen auch für die Ausbildung. Ja, man hat auch mal ein bisschen schwierigere Kosten. Wir
sind ja auch Anwendungsentwickler und nicht irgendwie, ja, was auch immer. Das ist ja unser
Beruf, also wir müssen uns damit eher auseinandersetzen. Und auch für alle anderen IT-Berufe,
ich habe es gesagt, empfehle ich das. Und wenn man sich nur die Einführungskapitel durchliest,
was ist überhaupt eine Funktion, was ist eine Funktion höherer Ordnung und so weiter, dafür
eignet sich das Buch auch sehr, sehr gut, so als Nachschlagewerk. Noch mal eben kurz auf ein,
zwei Seiten nachlesen, was war noch mal eine Funktion höherer Ordnung. Und das ist da wirklich
sehr, sehr schön definiert und mit Beispielen auch hinterlegt. Und dafür finde ich es auch eine,
ja, eine sehr gute Lektüre, auch gerade noch mal für die Prüfungsvorbereitung, wenn ich noch mal
gezielt die Vor- und Nachteile der funktionalen Programmierung oder überhaupt die Ideen der
funktionalen Programmierung mir anschauen möchte. Okay, dann würde ich sagen, wir gehen mal grob
über das Inhaltsverzeichnis, was alles so im Buch zu finden ist. Es geht los mit Kapitel 1,
Einleitung. Einleitung ist sonst ein Kapitel, was man häufig mal überspringen kann, in diesem Fall
aber nicht, denn da sind die elementaren Konzepte und Begriffe der funktionalen Programmierung drin
erklärt und auch schon der erste Hinweis auf FP in Java. Also da sind wirklich viele Sachen drin,
gerade so absolute Basics, die wirklich kurz und knackig noch mal definiert werden, damit man
direkt einsteigen kann in die späteren Kapitel. Also finde ich richtig gut, auf einem guten Niveau,
auch für Azubis verständlich erklärt. Dann kommen wir noch mal zu sprachlichen Grundlagen. Das
Kapitel 2, da gibt es noch mal ein sehr langes Kapitel zum Thema Java Generics. Und Generics,
also generische Klassen, ist auch so ein klassisches Prüfungsthema, was viele Prüflinge
nicht so auf dem Schirm haben. Und hier wird noch mal wirklich rauf und runter alles erklärt,
inklusive eines meiner Lieblingspunkte, und zwar Co- und Kontravarianz. Wer das in der Prüfung
erklären kann, der kriegt wahrscheinlich eine 1 mit Sternchen, wenn der Rest dann auch noch passt.
Aber alle möglichen anderen Sachen, wie Typ-Inferenz und Typ-Constraints und so weiter,
alles wird hier erzählt. Auch die Neuerungen in Java 8, also Default-Methoden-Interfaces zum
Beispiel. Und was ist überhaupt ein Lambda-Ausdruck? Wofür brauche ich sowas? Was ist ein Closure? Und
dann noch mal Funktionalinterfaces, die ja so ein bisschen die Grundlage für Lambda-Ausdrücke
in Java sind. Das heißt, da ist richtig viel drin, gerade für Java-Entwickler. Ja, richtig
schön zuckeriges Kapitel, sag ich mal. Ja, dann geht es weiter zum etwas allgemeineren Thema
Programmieren ohne Seiteneffekte. Das ist Kapitel 3. Da geht es dann um die Grundlagen der
funktionalen Programmierung, also reine Funktion, referenzielle Transparenz, Ausnahmebehandlung,
also Exception-Handling in funktionalen Sprachen und funktionale Datenstrukturen wie Liste und
Tuple und so weiter. Also hier geht es meiner Meinung nach sehr allgemein. Jetzt wirklich um
die Details der funktionalen Programmierung kann ich auch sehr empfehlen. Und danach geht es weiter
in Kapitel 4 mit Programmieren mit Funktionsparametern. Das heißt, hier geht es jetzt zum
ersten Mal um Funktionen höherer Ordnung, also Funktionen, die andere Funktionen als Parameter
bekommen oder zurückgeben. Was bringt mir das überhaupt? Wofür kann ich das anwenden? Und dann
auch wirklich mit konkreten Beispielen. Ich hatte gerade schon gesagt, die Tiefensuche zum Beispiel
oder an verschiedenen Entwurfsmustern, Strategie, Kommando und Besucher werden hier im Detail zum
Beispiel erklärt und wie man es dann funktional ausführen würde. Und auch ganz viele coole Sachen,
die irgendwann hoffentlich mal in Java reinkommen, wie zum Beispiel das Pattern-Matching, was hier
auch erklärt wird und Bedarfsauswertung, das heißt ja Lazy Evaluation, dass die Datenstrukturen erst
dann ihre Arbeit verrichten, wenn es wirklich angefordert ist. Wie man zum Beispiel auch
unendliche Folgen mit funktionalen Datenstrukturen abbilden kann und sowas. Ja, die absoluten Basics
und richtig gut erklärt. Ich habe schon ein paar mal gesagt. Dann geht es weiter in Kapitel 5.
Kombination von Funktionen. Fluent Interfaces. Wie baue ich die? Funktionen, die andere Funktion
wieder zurückgeben, die dann wieder was aufrufen und so weiter. Also Aufrufketten. Und dann aber
auch immer schön mit Beispiel in Java, also zum Beispiel Comparator Interface oder auch das
Collector Interface, was hier benutzt wird, um dann zum Beispiel Listen aus Streams zu machen
und so weiter. Also richtig viele interessante, spannende Sachen drin. Und dann kommt es zum
Hardcore, sage ich mal. Das ist das Kapitel 6. Funktoren, Monoide und Monaden. Also wenn man
die drei in der Prüfung auseinanderhalten kann, der hat seine eins, glaube ich, sicher. Der Freie
auch gar nichts anderes mehr. Das sind jetzt, hört sich so so hochtrabend an, die Begriffe,
aber die sind hier richtig verständlich und gut erklärt. Also eigentlich im Prinzip anhand
einer Klasse und welche Methoden die anbieten muss. Und dann weiß man, was ein Funktor, ein
Monoid oder eine Monade ist. Richtig schön gemacht. Und es wird auch gezeigt, wofür man
die braucht und was die Vorteile sind. Wie man die dann miteinander verketten kann, wenn man
mehrere Strukturen hat, die zum Beispiel eine Monade abbilden. Richtig schön. Auch mit Beispiel
aus Java für Monaden zum Beispiel das Optional. Das kennst du vielleicht schon. Also eine
Datenstruktur, die kennzeichnet, dass es einen Wert gibt oder eben nicht. Das ist das Optional.
Und eine andere Monade wäre zum Beispiel der Stream, also die Iteration über verschiedene
Elemente. Und das wird hier gleich auch im nächsten Kapitel 7 nämlich aufgenommen,
des Streams. Da geht es jetzt darum, wie man wirklich in der Praxis damit arbeitet. Also
klassische Map-Filter-Reduce-Geschichten, wie wandle ich Elemente um, wie reduziere
ich die auf einen Wert und so weiter. Das wird hier alles im Detail erklärt. Und das dürfte
dir auf jeden Fall für deine praktische Programmierung in Java sehr viel bringen,
falls du vielleicht noch nicht ganz so viel damit gearbeitet hast und eher so, weiß ich nicht,
Vorschleifen oder Enhanced-For, also For-Each-Schleifen baust. Dann hilft dir das Kapitel
sicherlich, um etwas funktionaler zu werden. Also eben mit Map-Filter-Reduce zu arbeiten,
statt die ganze Iteration immer selbst zu programmieren. Dann geht es auch weiter im
Anschluss. Kapitel 8 geht es um parallele Streams. Auch sehr schön, denn ein wichtiger Grund,
warum die funktionale Programmierung heutzutage so an Fahrt aufnimmt, ist ja, weil ich die gut
parallel ausführen kann. Vorausgesetzt, mein Problem lässt sich parallelisieren. Ja,
das ist alles richtig. Wird hier auch nochmal erklärt, dass man darauf achten muss und was
das überhaupt heißt, dass es parallelisierbar ist. Und dann wird aber auch am Beispiel des
Streams erklärt, wie man das denn genau machen würde und tatsächlich auch nochmal die Trade-Offs
gezeigt. Also ist es wirklich schneller, wenn ich mein Problem parallel bearbeite? Oder ist
der Overhead nicht so groß, dass es sogar langsamer wird? Da gibt es auch im Anhang
des Buches eine detaillierte Aufstellung mit großen Tests, um zu schauen, ob die
parallele Ausführung wirklich schneller ist. Also sehr, sehr gut gemacht und sehr
wissenschaftlich begründet und gezeigt, dass es gar nicht immer besser ist, wenn man alles
parallel ausführt. Und dann geht es weiter zum Thema Asynchronität. Da geht es jetzt konkret
in Java mit der CompletableFuture-Klasse darum, wie ich mit Ergebnissen umgehe, die irgendwann
in der Zukunft mir erst bereitstehen. Deswegen heißt das Ding ja Future, also klassische
asynchrone Programmierung. Ich drücke auf den Button zum Download von irgendeiner großen Datei
und in der Zeit soll meine App zum Beispiel ja nicht blockieren, sondern ich will weiter arbeiten
können. Das heißt, das Ding muss irgendwie im Hintergrund laufen und asynchron mir zurückmelden,
wenn der Download zum Beispiel fertig ist. Und hier gibt es eine sehr schöne Einführung,
warum ich sowas brauche, wie man sowas umsetzt und dann direkt am konkreten Beispiel der Reactive
Streams in Kapitel 10. Da geht es dann konkret um RxJava. Das Framework hast du vielleicht schon
mal aus einem anderen Umfeld gehört. RxJS gibt es zum Beispiel auch für JavaScript. Das ist also
ein Prinzip, wie ich reaktiv mit solchen Streams umgehe, also mit asynchronen Ergebnissen oder
Ereignissen und wie ich die dann ja im Prinzip mit MapFilterReduce verarbeiten kann. Und das
ist hier sehr ausführlich erklärt, wie das funktioniert, wie die Klassen heißen, was die
machen, wofür die da sind und auch wieder natürlich Vor- und Nachteile. Wann mache ich
das denn überhaupt? Wo bringt mir das tatsächlich wirklich was? Und auch wieder immer schön mit
Java-Beispielen angereichert. Also hat mir sehr gut gefallen. Dann Kapitel 11. Testen finde ich
auch super, super wichtig. Ganz unabhängig vom Programmierparadigmen. Testen ist immer wichtig,
aber funktionale Programme kann ich vielleicht ganz anders testen als objektorientierte. Und
hier wird noch mal so ein bisschen zum Beispiel auf die JUnit 5 Möglichkeiten eingegangen und
AssertJ als Matcher-Bibliothek. Und dann aber auch ein Ausblick auf das QuickCheck-Verfahren,
also Property-Based-Testing heißt es ja, also eigenschaftsbasierte Tests, was völlig anders
funktioniert als die klassischen JUnit-Tests, die du vielleicht kennst. Bei JUnit-Tests denkst du
dir die Werte, die reingehen sollen, in die zu testende Methode selber aus und definierst dann
auch selbst, was rauskommen muss. Das ist klassisch expected, das, was rauskommen muss,
und actual, das, was die Methode wirklich zurückgibt. Und Property-Based-Testing geht einen ganz anderen
Weg. Da werden zufällige Eingangswerte generiert und demnach kannst du auch nicht mehr sagen,
was am Ende wirklich rauskommen muss, sondern du musst quasi eine Eigenschaft festlegen des
Ergebnisses. Das heißt, anstatt zum Beispiel zu sagen, 3 plus 5 muss 8 ergeben, das wäre ein
JUnit-Test mit einem konkreten Beispiel, sagst du sowas, wenn ich zwei Zahlen addiere, soll das
Ergebnis größer sein als die eine Zahl zum Beispiel. Muss man natürlich gucken, ob das passt,
wenn die Zahl negativ ist, was ist dann, also ist immer schwierig, aber nur mal so vom Verständnis
her. Du sagst nicht A plus B gleich C, sondern du sagst, wenn ich zwei Zahlen addiere, dann muss
das Ergebnis eine gewisse allgemeine Eigenschaft aufweisen und das prüft dann das Framework,
ob das wirklich für alle möglichen Kombinationen an gültigen Zahlen zum Beispiel der Fall ist.
Macht natürlich nicht unendlich oft für alle möglichen Kombinationen, denkt sich dann ein
paar aus und da gibt es auch eine Möglichkeit, um zum Beispiel die Ergebnisse, wenn das Ding
fehlschlägt, auf die kleinstmögliche Zahl zu reduzieren, für die der Test fehlschlägt.
Ist jetzt ein bisschen schwer zu erklären in einem Podcast, aber das Buch erklärt es ja sehr gut. Das Ganze nennt sich dann Shrinking,
also ja, verkleinern. Und das bedeutet, ich fange mit irgendwelchen Testwerten an und wenn ich sehe, das schlägt fehl, dann
reduziere ich die Testwerte so weit, bis ich gerade noch einen Fehler produziere,
sodass es für den Entwickler am Ende einfacher ist, herauszufinden, was der Fehler ist. Ja, wenn ich also einen Fehler habe bei
der Zahl 2 Milliarden, dann ist das vielleicht schon ein Fehler, der ab der ersten positiven Zahl auftritt.
Das heißt, bei 2 Milliarden wäre das geschrinkte Ergebnis dann halt die 1. Und so kann der Entwickler sehen,
es geht um eine positive Zahl und nicht zwangsläufig um eine riesengroße Zahl zum Beispiel. Das erleichtert dann die Fehlersuche.
Jetzt sind wir aber schon fast mitten in der Erklärung, was das ganze Zeug eigentlich bedeutet. So weit wollte ich hier noch gar nicht gehen,
aber jetzt haben wir es abgehakt.
Damit wären wir jetzt aber auch durch. Bleibt noch ein Kapitel übrig, weiterführende Konzepte. Das ist Kapitel 12. Da kommen noch mal so ein paar
Ergänzungen, was man jetzt noch machen kann, wenn einem das Buch nicht ausgereicht hat zum Beispiel, ja. Und vor allem auch, was für coole Möglichkeiten
andere Sprachen bieten, die dann hoffentlich irgendwann mal in Java eingebaut werden. Ich nehme mal nur ein Beispiel, dieses Pattern Matching. Das ist ja schon
angedacht für Java, zumindest im Jahr 2020. Und das kommt bestimmt irgendwann. Und hier sind noch mal ein paar coole Features drin aus anderen Sprachen, die
eventuell irgendwann so in Java landen oder vielleicht auch nicht. Und dann können wir einfach traurig sein, dass andere Sprachen so cool sind und Java nicht.
Aber egal. Hauptsache, man hat sie mal gehört und weiß, was es damit auf sich hat.
Gut. Also, du hörst, ich finde das Ding richtig, richtig gut. Bei jedem Kapitel kannst du wirklich irgendwas mitnehmen für deine Ausbildung oder auch wenn du
selber Ausbilder bist unbedingt und vielleicht noch mal ein bisschen vertiefen möchtest, wie das ganze Zeug eigentlich funktioniert oder funktionieren sollte,
kauf das Buch. Es ist wirklich richtig gut. Ich kann das absolut weiterempfehlen. Und irgendwann in der nächsten Zeit werde ich es auch mit meinen
Azubis lesen. Das habe ich bislang noch nicht gemacht. Ich habe es selber erst gerade gelesen und gesehen, dass es richtig gut ist. Deswegen werde ich es
jetzt mit meinen anderen Azubis lesen. Aber das muss ich natürlich auch zeitlich noch irgendwo unterbringen. Ansonsten kauft das Ding und lest es. Es ist wirklich richtig gut.
Und jetzt würde ich zum zweiten Teil übergehen. Und zwar anhand der
Sachen, die im Buch erklärt werden, mal so ein bisschen funktionale Programmierung noch mal durchgehen. Ich hatte dazu schon mal ein Podcast aufgenommen. Da hatte
ich ein Interview mit Gregor Trevs. Das war die Episode 106. Da kannst du auch gerne noch mal reinhören. Da geht es auch um
funktionale Programmierung und so ein paar allgemeine Sachen dazu. Und heute möchte ich noch mal die Sachen wirklich im Detail einzeln durchsprechen.
Ich versuche mich auch kurz zu halten. Ich kann es jetzt nicht so machen, wie das Buch mit seinen, ich weiß nicht wie viel, 100 Seiten.
Ich versuche kurz und knackig die Sachen zu erklären. Wenn du einzelne Sachen davon nicht verstehst,
liest das Buch oder recherchiert im Internet oder probiert es selber aus. Die Sachen, die ich jetzt durchgehe, das sind Dinge, wo ich glaube, die könnten in
einer Prüfung abgefragt werden. Mindestens in einer mündlichen Prüfung für Anwendungsentwickler.
Gut, fangen wir mal vorne an. Ich habe das Ding mal Grundlagen genannt. Und da sind ja solche Sachen drin wie Lambda-Ausdrücke, Typ-Inferenz,
algebraische Datentypen, Pattern-Matching, Generics und Co- und Kontra-Variants. Gehen wir mal der Reihe nach die Sachen durch.
Lambda-Ausdrücke sind anonyme Funktionen.
Punkt. Das war es eigentlich schon. Also wenn du das definieren müsstest, eine Funktion, die anonymes, was heißt das? Die hat keinen Namen.
Daran erkennst du das. Das heißt, du hast normalerweise Methoden, die du aufrufst in deiner Objektorientierung. Die heißt dann irgendwie, keine Ahnung,
Hupe beim Auto. Und wenn du jetzt aber eine Funktion definierst, die keinen Namen hat, dann ist sie halt anonym.
Du kannst sie nicht beim Namen nennen, was ja quasi die Definition von anonym ist.
Und Lambda-Ausdrücke haben jetzt den Vorteil, dass du eben nicht den ganzen Rumsch für eine normale Methode bzw. hier geht es ja um Funktionalprämierung,
also Funktionen brauchst, sowas wie den Funktionskopf und den Rumpf und Return und ich weiß nicht was alles, sondern es ist eine kurze
knackige Definition einer Funktion
ohne eben diesen ganzen Overhead, den du sonst einfach hast. Das heißt, wenn du zum Beispiel die Funktion nur ein einziges Mal benutzen möchtest,
warum musst du sie dann in eine umständliche Methode mit Klammer auf und diesem ganzen Kram packen? Brauchst du nicht.
Und in vielen Programmiersprachen sind diese Lambda-Ausdrücke mit so einem Pfeil in der Mitte definiert. Das heißt, die Funktion ist eigentlich schon
eine vollständige Definition, wie du sie auch kennst. Das heißt, du hast Eingangsparameter und einen Rückgabetypen. Das gibt es auch bei Lambda-Ausdrücken.
Aber die musst du nicht explizit hinschreiben. Das heißt, du könntest eine Funktion definieren, die einfach so aussieht a, b
Pfeil a plus b. Das wäre eine Funktion, die a und b addiert.
Ja, das heißt, hier steht nirgendswo Function, Add, Klammer auf, Integer, sonst irgendwas steht da alles nicht, sondern wirklich nur die
Variablen, die Parameter, die Eingangsparameter und sogar der Return-Wert, das a plus b, ist implizit. Das heißt, man muss nicht mal
return a plus b da hinschreiben. Auch das würde diese Syntax erlauben.
So, in Java gibt es jetzt noch Details, wann muss ich Klammern schreiben, wann nicht. Das, was ich gerade beschrieben habe, ist so die kürzeste Variante.
Es gibt noch verschiedene Zwischenvarianten davon, aber halten wir uns damit mal nicht länger auf. Denn damit das Ganze so funktioniert, ist ein weiteres wichtiges
Sprachfeature wichtig und das ist die Typ-Inferenz.
Denn wenn ich jetzt einfach schreibe a, b, Pfeil a plus b, woher weiß der Compiler, welche Datentypen a und b haben?
Das muss er wissen in statisch typisierten Sprachen wie Java. Das heißt, woher kriegt er die Information?
Ja nun, er kann sie ableiten und zwar aus dem Kontext und das Ganze nennt man Typ-Inferenz. Das heißt, wenn du Datentypen
implizit aus dem Kontext des Codes ableitest, beziehungsweise der Compiler das macht, dann ist das eine sogenannte Typ-Inferenz. Wie funktioniert das?
Stell dir vor, du hast so etwas wie eine Variablen-Deklaration, der du gleich einen Wert zuweist. Also, das würdest du üblicherweise machen,
String a gleich Text und Text in Anführungszeichen, ja. Jetzt weiß der Compiler eigentlich, wenn er sich die rechte Seite anguckt,
Text in Anführungszeichen, huh, das ist ja ein String.
Das heißt, es sollte die Variable auf der linken Seite jetzt eigentlich auch ein String sein und siehe da, links steht ja auch
String a. Also, passt zusammen und funktioniert.
Aber wenn ich das jetzt weiß, warum muss ich das dann doppelt hinschreiben? Warum muss ich String a
hinschreiben, wenn ich aus der rechten Seite schon ableiten kann, dass die Variable gar kein anderer Datentyp als String sein kann?
Und das muss ich nicht. Das kann die Sprache, beziehungsweise der Compiler für mich prüfen und das ist die Typ-Inferenz.
Das heißt, der Compiler guckt auf der rechten Seite der Zuweisung. Aha, da steht ein String.
Das heißt, die Variable auf der linken Seite muss ein String sein und dann musst du es auch nicht mehr explizit hinschreiben.
Achtung, das heißt nicht, dass der Typ nicht festgelegt ist. Wir bleiben in der statischen Typisierung. Das ist ganz wichtig, wenn du dir seit Java 11
gibt es die Typ-Inferenz für lokale Variablen. So was hinschreibst wie var a
gleich Text in Anführungszeichen, dann heißt das var, also variable, V-A-R, die Abkürzung, nicht, dass du jetzt eine
dynamische Variable oder sowas hast, die ihren Typ ändert. Nein, nein, nein, das ist auf jeden Fall ein String. Du musst es bloß nicht
hinschreiben. Du sparst dir also Tipparbeit und genau darum geht es eigentlich.
Alles, was du nicht hinschreiben musst, weil der Compiler es für dich ableiten kann, macht dir das Leben einfacher.
Und deswegen ist die Typ-Inferenz ganz cool. Du musst deine Variablen eigentlich nur noch mit var
deklarieren und dann wird der Typ dieser Variablen automatisch abgeleitet, dann aber auch in Stein gemeißelt. Also a
ist danach ein String, auch wenn es nicht explizit da steht, aber du kannst dann nicht in der nächsten Zeile machen
a gleich 5 oder so und ein Integer reinschreiben. Also ist ganz normal statisch typisiert. Du musst es bloß nicht hinschreiben und das ist ja genau diese
Definition von
Impliziter Ableitung des Datentypen. Implizit heißt immer, das steht nicht explizit im Code. Du musst nicht hinschreiben String a,
sondern da steht einfach var a und String wird quasi abgeleitet, ohne dass du es siehst. Das ist quasi unsichtbar und der Compiler leitet das
für dich ab.
So, dann haben wir noch so ein paar schöne neue Datenstrukturen oder neu, weiß ich nicht, aber diesen Begriff hast du vielleicht noch nicht gehört.
Algebraische Datentypen. Das hört sich natürlich wieder richtig geil an. Kannst du Leute mit beeindrucken, aber letztlich ist es
eigentlich ganz einfach. Es gibt zwei Untertypen, und zwar die Produkttypen und die Summentypen.
Deswegen auch Algebra. Produkt und Summe kennst du aus der Algebra, aus der Schule. Und ein Produkttyp, das ist letztlich nichts anderes als unsere
Objekte, die wir schon aus der Objektorientierung kennen, beziehungsweise
Structs oder Records oder wie auch immer die in anderen Sprachen heißen. Und das ist im Prinzip eine Definition des Datentyps durch die
Kombination verschiedener anderer Datentypen. Wenn du zum Beispiel sagst, ein Auto, das ist ein eigener Datentyp, der besteht aus einem String,
Marke und einem Integer, Kilometerstand, dann hast du String und Integer jetzt quasi
kombiniert, quasi zu einem Produkt gemacht und daraus dann deinen eigenen Datentyp Auto
definiert. Und Produkt deswegen, weil jetzt quasi der Datentyp definiert, dass du verschiedene Werte dieser unterschiedlichen Datentypen in
deinem Auto drin miteinander kombinieren kannst. Deswegen heißt das Produkt. Also du könntest jetzt sagen, du hast einen VW mit Kilometerstand
3 und ein Mercedes mit Kilometerstand 5000 oder was auch immer.
Durch diese Kombination wird der Datentyp als Produkttyp definiert.
Und der nächste Typ, das ist der Summentyp und den gibt es so in Java zumindest noch nicht, in anderen Sprachen aber schon.
Und da wird der Datentyp definiert als Liste möglicher
Alternativen, beziehungsweise sogenannter
Varianten. Das hast du vielleicht in anderen Sprachen schon mal gesehen. Das ist nicht das gleiche wie eine Enumeration. Das ist ein Aufzählungstyp, wo du sagst,
Wochentage sind und dann hast du eine Liste erlaubter Werte, Montag, Dienstag, Mittwoch und so weiter. Das ist nicht gemeint, sondern es geht wirklich um
einen Datentyp, wo du sagen kannst, der Datentyp ist entweder Typ A oder Typ B oder Typ C.
Ich habe mal ein Beispiel aus der Versicherung mitgebracht. Du könntest sagen, ich definiere den Datentyp Krankenversicherung als entweder eine
gesetzliche Krankenversicherung oder eine private Krankenversicherung.
Diese beiden Klassen müssen auch nichts miteinander zu tun haben. Die müssen nicht in einer Vererbungsbeziehung stehen oder sonstiges oder ein Interface implementieren. Das können
beliebige Klassen sein und du definierst halt einen Datentyp, der einen beliebigen Datentyp dieser Liste
annehmen kann. Wofür braucht man das? Das ist eine gute Frage. Im Buch wird das Ganze mit einem Beispiel
eines Ausdrucks aus der Programmiersprache gemacht. Wenn du dir einen bultschen Ausdruck mal vorstellst, kennst du aus der Programmierung
True, zum Beispiel, wäre ein bultscher Ausdruck. Oder
A größer 5 ist auch ein bultscher Ausdruck, weil immer ein bultsches Ergebnis rauskommt. Und daran siehst du schon, für den Compiler bzw.
den Parser des Codes
ist ein bultscher Ausdruck True
genauso ein bultscher Ausdruck wie eben A größer 5, obwohl die beiden Ausdrücke an sich ja relativ unterschiedlich sind. Das eine ist ein Literal,
True. Das andere ist eine Kombination aus einem Vergleich einer Variablen mit einem
Literal, nämlich mit 5. Also True ist ein einziges Wort, wenn du so willst. Und A
größer 5 ist eine Kombination, ein Ausdruck aus mehreren Bestandteilen. Trotzdem sind beide bultscher Ausdrücke. Das heißt, du könntest jetzt sagen, ein bultscher Ausdruck ist
entweder ein Literal oder
ein Ausdruck mit einem Integer und einem irgendwas und sonst wie. Das heißt, du kannst beliebige Datentypen
quasi, ja, hintereinander schreiben und als Alternativen deklarieren und einem gemeinsamen
sogenannten Summentyp dann eben zuordnen. Und dann kannst du in deinem Programm so was machen wie, hey, mein Summentyp gleich True.
Das würde genauso gehen wie mein Summentyp gleich A größer 5.
Und sowas ist insbesondere dann interessant, wenn ich Klassen zum Beispiel einem gemeinsamen Datentyp zuordnen will, die aber nicht irgendwie
technisch verbinden möchte, zum Beispiel über ein gemeinsames Interface oder eine gemeinsame Basisklasse. Das ist nicht immer sinnvoll, ja.
Mein Beispiel, was ich immer aus der Objektorientierung habe, ist ja Auto und Elefant. Beide kann ich waschen in einer Waschanlage, ja.
Trotzdem haben die beiden natürlich keine gemeinsame Oberklasse. Wie soll ich das nennen?
Ding und Auto erbt von Ding und Elefant erbt von Ding oder das ist irgendwie blöd, ne. Ich könnte in Java so was machen wie Interface
waschbar. Dann sage ich, das Auto ist waschbar und auch der Elefant ist waschbar. Kann ich tun? Ja, das geht so ein bisschen in die Richtung.
Ich könnte aber auch einfach sagen, hey, ich habe hier so ein
waschbares Ding und das kann halt entweder ein Auto sein oder ein Elefant und dann müssen die beiden Klassen noch nicht mal irgendwas miteinander zu
tun haben. Die müssen keine gemeinsamen Methoden haben oder sonst irgendwas. Durch diesen Summentyp bin ich da völlig frei in der Deklaration, welche
unterschiedlichen Ausprägungen dieser Typ dann haben kann.
So, das nächste Thema, was erläutert wird, ist das Pattern-Matching und das passt jetzt sehr gut zu diesen Datentypen, wie gerade zum Summentyp.
Denn wenn ich jetzt mir den Code mal angucke und ich arbeite mit so einem Summentyp und da kann entweder ein Auto drinstehen oder
ein Elefant,
wie soll ich denn dann damit umgehen? Denn ein Elefant muss ich ja ganz anders waschen als ein Auto.
Ja, es ist ein seltsames Beispiel, aber beim Auto zum Beispiel muss ich erst die Fenster zumachen und dann gehe ich von draußen mit einem heißen
Dampfstrahler dran oder so.
Beim Elefant geht das vielleicht nicht. Da muss ich einen Schwamm nehmen, weil er sich erschreckt, wenn ich den irgendwie mit so einem Dampfstrahler
anpuste und ich muss vielleicht erst seine
Pfoten, wollte ich schon sagen, wie heißen die denn? Seine großen Füße
waschen, ja, und dann vielleicht einmal
macht er den Rest mit dem Rüssel selber, was weiß ich, ja? Das heißt, die Implementierung ist ja komplett unterschiedlich, je nachdem, was ich da
waschen möchte. Und darauf muss ich halt in meiner Programmiersprache reagieren können. Das heißt, ich muss jetzt entscheiden,
hey, das Ding, was ich gerade waschen soll, was ist denn das jetzt? Ein Auto oder ist das ein Elefant? Und abhängig davon verhält sich mein
Code unterschiedlich. Und das Ganze als Pattern-Matching umgesetzt, kann man sich jetzt so vorstellen, dass du im Prinzip ein großes Switch-Statement hast
und sowas sagst wie, hey, wenn der Datentyp ein Auto ist, mach das und das,
wenn der Datentyp ein Elefant ist, mach das und das, und wenn er nichts von beiden ist, dann schmeiß einen Fehler, zum Beispiel.
Das könntest du auch in Java heute schon machen mit einem Switch-Statement, ja?
Im Prinzip ist es das Gleiche, aber Pattern-Matching macht jetzt noch ein bisschen mehr.
Denn wenn du zum Beispiel, wenn du das Auto waschen willst, vorher wissen musst, wie hoch es ist, ja, dann könntest du in diesem
Pattern-Matching sogar sagen, wenn das Ding ein Auto ist und die Höhe
größer 1,50 Meter, dann mach das und das, wenn das Ding ein Auto ist und die Höhe kleiner
1,50 Meter, dann mach das und das. Das heißt, du kannst nicht nur die Typen unterscheiden in diesem Switch-Statement, sondern du kannst in die konkreten Objekte auch
reingucken und nicht nur sagen, welche Klasse ist das, sondern welche Werte haben die einzelnen Attribute dieses Objekts zum Beispiel, um dann
darauf aufbauend eine Fallunterscheidung zu machen. Das heißt, dieses Pattern-Matching, das zerlegt die Objekte wirklich quasi in seine Bestandteile,
kann das dann auch automatisch einzelne neuen Variablen zuordnen. Wenn du zum Beispiel die Höhe einer Variablen brauchst und damit dann irgendwas zu tun,
macht das Pattern-Matching das automatisch, ja, liest die Höhe aus aus dem Objekt und legt dir eine neue Variable an, wo die Höhe drin steht.
Das heißt, kannst du alles in einer Zeile Code quasi machen, um diese Objekte auseinander zu friemeln, um dann damit
weiterzuarbeiten. Das heißt, hört man auch immer gerne sowas wie, das ist ein Switch-Statement on Steroids.
Das heißt, das Ding kann viel, viel, viel mehr als einfach nur, wenn eins, mach das, wenn zwei, mach das, sondern wie gesagt, er geht in die
Objekte rein und kann die auseinandernehmen.
So, dann das nächste Thema, Generics oder auch generische Klassen. Schon seit Jahren eigentlich ein beliebtes Prüfungsthema.
Trotzdem gibt es viele, die damit nicht so ganz
umgehen können und vor allem nicht erklären können, was es ist. Grundsätzlich sind Daten, sind
generische Klassen ja einfach Klassen, die, wie der Name sagt, generisch, also allgemein gültige
Algorithmen implementieren. Schönes Beispiel aus Java oder auch aus so ziemlich allen anderen Objektnotierten Sprachen ist eine Liste. In eine Liste kann ich
alle möglichen Objekte reinpacken, zum Beispiel Autos, Elefanten, Strings, Integer, was auch immer, ja.
Und trotzdem macht die Liste immer das Gleiche, nämlich, die enthält mehrere Elemente dieses Datentyps.
Und jetzt müsste ich, wenn ich sowas nicht machen könnte, eigentlich für jeden meiner Datentypen eine eigene Liste implementieren, also
Elefantenliste, Autoliste, Integerliste, denn das muss ja statisch typisiert sein.
Und diese Klassen, die ich da bauen würde, wären exakt identisch, nur dass sich der Datentyp, mit dem sie arbeiten, als inneres Element
unterscheiden würde. Und das wäre ein riesen Overhead. Deswegen bieten eigentlich alle guten, statisch typisierten Sprachen solche Generics an.
Nicht alle, ich habe schon gehört, Go, glaube ich, bietet das nicht, aber C Sharp und Java hat es auf jeden Fall.
Denn dort kann ich einfach sagen, ich implementiere die Liste ein einziges Mal und statt zu sagen, in der Liste sind nur Integer drin, sage ich
einfach, es ist irgendein Typ in dieser Liste drin. Und diesem Typen gebe ich dann einen Platzhalter, das wird dann meistens T
genannt, deswegen heißt die Liste dann auch nicht mehr einfach nur List, sondern List of T und T steht dann so in Spitzenklammern
dahinter. Das bedeutet, diese Klasse kann mit einem anderen Datentyp
parametriert werden. Deswegen heißen Generics auch
parametrierbare oder
parametrisierbare, je nachdem, wie man das aussprechen will, Klassen. Das heißt, du definierst eine Klasse Liste, die mit beliebigen Datentypen umgehen kann.
Und wenn du jetzt eine konkrete Liste benutzen willst, dann sagst du ihr, hey, du kannst bitte nur Integer
aufnehmen. Und ab dann sind auch alle Methoden
typsicher, geben dir Integer zurück, erwarten Integer als Parameter und so weiter. Aber wenn du dir die Klasse selber anguckst, die List of T,
dann steht da nirgends so was von Integer oder String oder sonst wo, sondern nur dieser Platzhalter für den konkreten Datentypen, also T meistens.
So, diese Generics sparen dir einen Haufen Arbeit, weil du
ja, das Beste aus beiden Welten hast. Du hast die statische Typisierung, du hast eine Liste, in die du nur
Integer packen kannst und der Compiler verhindert was anderes. Auf der anderen Seite musst du nicht extra die Integer-Liste
implementieren, sondern kannst die Liste einmalig für alle beliebigen Datentypen
implementieren. Das ist eine schöne Geschichte und das sollte man auf jeden Fall verstanden haben und vermutlich nutzt du das sowieso schon seit
Jahren, wenn du schon länger programmierst in deiner Programmierung.
Und wenn du irgendwie ArrayListOfT zum Beispiel benutzt oder in C-Sharp heißt das Ding ja direkt ListOfT.
Das solltest du auf jeden Fall verstanden haben, was dahinter steckt.
Und im Zusammenhang mit diesen generischen Klassen wird jetzt im Buch auch Co- und Kontravarianz erklärt und das sind
ja
richtig coole Sachen. Wenn man die gut erklären kann als Prüfling, dann hast du auf jeden Fall ein Stein im Brett bei den Prüfern.
Und zwar geht es hier darum, wenn wir bei den generischen Klassen mal bleiben, wie sich
verschiedene generische Klassen, die typisiert sind mit diesen diesen Datentypen, die ich da
reingebe, wie die sich untereinander verhalten, ob die quasi
ersetzbar sind durcheinander, also quasi ob die in einer Vererbungsbeziehung stehen. Ich erkläre es mal einem konkreten Beispiel.
Angenommen, du hast eine Liste aus
Personen und du hast eine Liste aus
Studenten. Studenten sind Personen. Wenn dazwischen eine Vererbungsbeziehung besteht, würdest du sagen class Student extends
Person. Das heißt, Student ist eine abgeleitete Klasse von Person.
Steht in einer Vererbungsbeziehung. Wenn du dir jetzt anguckst, Liste von Person und Liste aus
Studenten. Stehen die auch in einer Vererbungsbeziehung? Wenn der Typ-Parameter in einer Vererbungsbeziehung steht,
Student erbt von Person, erbt dann auch die Liste von Student, von der Liste aus Person.
Und das ist leider nicht der Fall. Zumindest nicht in Java. In anderen Polymer-Sprachen ist das so. Und genau darüber nähern wir uns jetzt
Ko- und Kontra-Varianz. Wenn das so wäre,
dass die Liste aus Studenten von Liste aus Personen erben würde, dann würden wir von einer
Ko-Varianz sprechen. Ko-Varianz deswegen, weil die Vererbungsbeziehung des
parametrierten Typs, das ist Student und Person, weil diese Vererbungsbeziehung in die gleiche Richtung geht, wie die Vererbungsbeziehung zwischen den Listen.
Also List of Student erbt von List of Person,
weil Student von Person erbt. Also die gleiche Vererbungsrichtung.
Wenn das umgekehrt wäre, würden wir von Kontra-Varianz sprechen. Das heißt, wir hätten sowas wie die List of Person
erbt von List of Student.
Weil student von person erbt, also student person und list of student person, genau umgekehrte
Vererbungsbeziehung, das wäre dann die contra-Varianz. Und co heißt ja immer gemeinsam in die
gleiche Richtung und contra ist immer dagegen, also in die Gegenrichtung ist die Vererbung
dann implementiert. Übrigens, wenn du dich jetzt gerade gewundert hast, dass die beiden
Listen nicht in einer Vererbungsbeziehung stehen, dann wäre es dringend an der Zeit,
das mal selber auszuprobieren. Du kannst es direkt nachvollziehen in Java. Wenn du so
was schreiben würdest wie list of person gleich new list of student, dann sagt der
Compiler, mähp, geht nicht. Das heißt, auch wenn die parametrierten Typen in einer
Vererbungsbeziehung stehen, die Listen stehen nicht in einer Vererbungsbeziehung. Das ist
der Standard in Java. Warum das so ist, das kann man sich am besten einem Beispiel erklären. Und
ich mache jetzt mal nicht co und contra-Varianz beides, weil ich habe schon über 40 Minuten hier
geschnackt. Das kannst du besser im Buch nachlesen. Aber ein Beispiel für die co-Varianz,
warum selbst die co-Varianz nicht funktioniert. Bei contra-Varianz könnte man noch sagen,
ja okay, die Vererbung ist ja völlig verdreht. Was soll das für einen Sinn ergeben? Aber die
co-Varianz, da könnte man eigentlich meinen, klar, wenn die voneinander erben, warum sollen
die Listen nicht voneinander erben? Aber ein einfaches Beispiel zeigt, dass das nicht sein
darf. Nehmen wir das Beispiel mit Person und Student. Wenn du sagst, ich definiere mir eine
Liste aus Personen ganz allgemein und weist dir aber eine Liste aus Studenten zu. Das wäre ja
co-Variant. Person und Student. Student erbt von Person, also muss die Liste der Studenten auch
von der Liste aus Personen erben. Hört sich erstmal sinnvoll an. Angenommen das ginge aber,
dann hätte deine Variable den Typ List aus Personen. Die konkrete Implementierung wäre
aber eine List aus Studenten. Wenn du jetzt aber mit der Variable arbeitest, wo nur List of Person
steht, dann kannst du dieser Liste ja auch eine Person hinzufügen und das muss ja nicht zwangsläufig
ein Student sein, das könnte ja vielleicht auch ein Lehrer sein oder ein Professor, wenn das auch
eine Person ist. Und jetzt würde folgendes passieren. Die Variable sagt, ich darf Personen
aufnehmen, also kannst du auch einen Professor hinzufügen. Sollte logisch sein. Die konkrete
Implementierung hinter der Variablen ist aber ja eine List aus Studenten und wenn du dir jetzt
einen Professor reingibst, dann sagt die Studentenliste, was willst du denn? Ich kann
aber nur Studenten aufnehmen. Und da siehst du schon, dass selbst die co-Varianz so nicht
funktionieren kann, weil die Listen intern mit diesen Datentypen nicht umgehen können. Du kannst
in die Studentenliste einfach keinen Professor packen, aber wenn die Variable sagt, ich erlaube
jede beliebige Person, dann passt das nicht zusammen. Und genau deswegen erlaubt der Java
Compiler das auch nicht. Du kannst das so nicht machen in Java, außer du kennst die Syntax, um die
Parameter so zu erweitern, dass das erlaubt ist. Und das sind die sogenannten Typ-Constraints. Da
könntest du sowas hinschreiben wie, hey, ich habe hier eine List aus Personen oder irgendeinem
abgeleiteten Typen. Und das würde dann so aussehen, dass du hast eine List of Fragezeichen
ExtentsPerson. Wenn du das noch nie gesehen hast, das ist die offizielle Syntax, um in Java quasi
Co-Varianz herzustellen. Deine Liste enthält irgendeine Klasse, die Person ist oder sie erweitert.
Also Fragezeichen ExtentsPerson, Person selbst oder irgendwas abgeleitetes. Und das wäre in diesem
Fall zum Beispiel der Student. Und dann würde das ganze Co-Variant funktionieren. Und auch für
die Kontra-Varianz, dass du das wenigstens schon mal gehört hast, das wäre in Java dann
Fragezeichen SuperPerson. Das wäre also irgendwas, was Person ist oder drüber. Das heißt in Java
zum Beispiel Object. Alles erbt von Object. Das heißt, da könntest du auch irgendein beliebiges
Object nutzen anstatt der Person. So, das waren die allgemeinen Sprachgrundlagen noch mal, die
wichtig sind, um dann den Rest zu verstehen. Im Prinzip zu verstehen, wie man das dann in Java
umsetzt und in anderen Objektorientierten Sprachen. Und jetzt kommen wir zu dem Prinzip hier in der
funktionalen Programmierung. Das wäre jetzt sowas wie, was sind die drei Säulen der Objektorientierung?
Vererbung, Polymorphik, Kapselung. Und jetzt kommen wir zu den Themen, was sind eigentlich die
grundlegenden Prinzipien der funktionalen Programmierung? Das sollte man zumindest
auflisten und auch grob erklären können, wie ich finde. Und das sind der Reihe nach Funktionsobjekte,
reine Funktionen, Funktionen höherer Ordnung, Bedarfsauswertung, Unveränderbarkeit, Rekursion
und referenzielle Transparenz. Nicht referenzielle Integrität, das kommt aus dem Bereich der
Datenbanken. Hier reden wir von referenzieller Transparenz. Fangen wir mal ganz oben an.
Funktionsobjekte oder auf Englisch auch First Class Citizens oder Functions as First Class Citizens
genannt. Das bedeutet, dass die Funktionen fester Bestandteil der Sprache sind und genauso benutzt
werden können wie zum Beispiel Klassen und primitive Datentypen und so weiter. Das heißt,
ich kann die Variablen zuordnen, ich kann die als Parameter irgendwo übergeben. Ich kann damit tun
und lassen, was ich will. Genauso wie ich es in Java zum Beispiel mit Objekten mache, kann ich es
hier auch mit Funktionen machen. Das heißt, das ist als Sprachkonstrukt wirklich in die Sprache
eingebaut und nicht irgendwie was, was drangeflanscht ist. Und da muss man jetzt zum Beispiel bei Java
sagen, das ist ein bisschen drangeflanscht, denn diese Funktionen, diese Lambda-Ausdrücke, da
werden im Hintergrund einfach anonyme Klassen generiert. Das heißt, die haben jetzt nicht
einfach komplett die Sprache auf Funktional umgestellt, sondern die Lambda-Syntax sorgt
nur dafür, dass eine anonyme Klasse mit einer Methode generiert wird und diese Klasse wird dann
quasi als Datentyp benutzt. Also die haben es wirklich ein bisschen drangeflanscht, aber andere
Sprachen haben halt wirklich das Prinzip einer Funktion in der Klasse selber integriert und da
musst du also nicht so hinten rum doch eine Klasse definieren, sondern du kannst einfach sagen, ich
will eine Funktion haben, zack, und der ganze Rumpf drumherum, der ist nicht notwendig, weil die
Funktion halt eben ein gebauter Bestandteil der Sprache ist. So, dann reden wir von reinen
Funktionen. Das geht so ein bisschen in die Richtung, wie wir programmieren wollen in der
funktionalen Programmierung. Wir wollen nämlich möglichst ohne Seiteneffekte programmieren. Was
sind Seiteneffekte? Das sind Dinge, die du als Aufrufer der Methode nicht sehen kannst. Das heißt,
mein Lieblingsbeispiel in Java ist System.out.println. Ich gebe was auf der Konsole aus. Die
Funktion, ach, die Methode println hat aber einen Void-Rückgabewert. Das heißt, es kommt nicht
zurück. Das heißt, ich als Aufrufer kriege überhaupt nicht mit, ob der Text jetzt wirklich
ausgegeben wurde. Ich kriege gar nichts zurück. Und als Seiteneffekt erscheint irgendwo auf der
Konsole hoffentlich ein Text. Und diese Seiteneffekte, die wollen wir in der
funktionalen Programmierung überhaupt nicht haben. Wir wollen stattdessen, dass sich Funktionen immer
identisch verhalten, wenn sie identische Eingangswerte bekommen für ihre Parameter. Wenn
ich also mal ein Beispiel habe, was nicht seiteneffektfrei ist, ein Objekt, was irgendwie
einen Zähler intern hat. Und jedes Mal, wenn ich eine Funktion aufrufe, wird dieser Zähler erhöht.
Das heißt ja, bei jedem Funktionsaufruf wäre der Wert ein anderer. Also nicht der gleiche,
wenn ich den gleichen Parameter habe. Und das ist genau so etwas, was wir nicht haben wollen.
Also Zustand in der Funktion, der führt genau dazu, dass ich eben keine reine Funktion habe,
weil das Ergebnis der Funktion nicht ausschließlich davon abhängt, was ich reingebe. Angenommen,
ich hätte eine Addiere-Funktion, wo ich zwei Parameter habe, a und b, und es kommt einfach a
plus b zurück. Dann wäre das eine reine Funktion. Denn wenn ich 3 und 5 reingebe, kommt immer 8 raus,
egal wie oft ich das mache und ob ich vorher schon was anderes aufgerufen habe. Es gibt keinen
Seiteneffekt. Wenn ich aber zum Beispiel sowas habe, wie dieses Objekt, was ich eben beschrieben
habe, was sich ein intern Zähler merkt und einfach bei jeder Addition diesen Zähler oben drauf
rechnet, also anstatt zu sagen return a plus b, macht er noch return a plus b plus Zähler. Und
der Zähler kann sich verändern. Dann ist eben nicht sichergestellt, dass 8 rauskommt, wenn ich
3 und 5 reingebe. Denn wenn der Zähler 2 ist, kommt 10 raus. Wenn der Zähler 3 ist, kommt 11
raus. Und da ich den Zähler nicht mit reingebe, ich ihn also auch nicht sehen kann, sieht das für
mich so aus, als wenn sich die Funktion unterschiedlich verhält. Und das tut sie ja
auch, obwohl ich das Gleiche reingegeben habe, nämlich 3 und 5. Und das soll nicht so sein,
denn solche Funktionen sind sehr schwer zu testen. Ich weiß nicht, was zurückkommen muss,
weil ich ja nicht alles definieren kann, was ich reingebe. Und das ist in der funktionalen
Formulierung nicht gut. Das wollen wir nicht haben. Wir wollen, dass die Funktionen ausschließlich
mit dem arbeiten, was sie reinbekommen, also ihren Parametern, und mir dann immer das gleiche
Ergebnis liefern, wenn ich auch die gleichen Eingangswerte reingebe. So, dann kommen wir zum
nächsten Prinzip. Das sind die Funktionen höherer Ordnung oder auf Englisch function oder higher
order function. Was bedeutet das? Das sind letztlich einfach nur Funktionen, die andere
Funktionen als Parameter bekommen oder als Rückgabewert haben. Wenn ich also die Additionsfunktion
von gerade nehme, da kommen einfach zwei Zahlen rein, A und B. Wenn ich jetzt aber eine Funktion
habe, die als Parameter eine Funktion bekommt, und ein schönes Beispiel dafür wäre Map. Wenn
du MapFilterReduce von Streams oder allgemein von der Verarbeitung mehrerer Elemente kennst,
dann weißt du, dass die Map-Funktion eine andere Funktion als Input hat, und zwar, um aus dem
Datentyp, der gerade in der Liste drin ist, einen anderen zu machen. Das heißt, angenommen, ich habe,
wie eben, meine ListOfPerson, und ich möchte von diesen Persons immer alle Vornamen haben,
dann muss ja aus der Person quasi ein String werden. Person liefert, wenn ich da irgendwie
GetVornamen oder so aufrufe, ein String als Vornamen zurück. Und wenn ich jetzt eine Map-Funktion
habe, wo ich sage, hey, ListOfPerson, Map, und da gebe ich eine Funktion rein, die aus der Person
den Namen macht, dann habe ich als Ergebnis eine ListOfString. Das heißt, ListOfPerson, Map und
dann sowas wie PersonGetFirstName, dann wird die Map-Funktion für jedes Element in der Liste
GetFirstName aufrufen und das wieder in eine neue Liste packen. Und so habe ich aus der
ListOfPerson eine ListOfString gemacht. Und damit das funktioniert, braucht diese Map-Funktion eine
andere Funktion als Parameter, und zwar die, die aus der einen Person den einen String macht. Und
die Map-Funktion kümmert sich dann darum, aus allen Personen alle Namen zu machen, indem sie
quasi die Iteration macht und die übergebene Funktion auf jedes Element der Liste anwendet.
So, mit diesem Prinzip der höherwertigen Funktionen oder higher-order functions kann
ich jetzt Funktionen zu komplexeren oder abstrakteren Funktionen zusammenbauen, weil
ich zum Beispiel wie die Map-Funktion nur ein einziges Mal definieren muss, wie ich über eine
Liste iteriere und Werte umwandle, und ich kann dann von draußen bestimmen, wie diese
Wertumwandlung stattfinden soll. Die nächste Funktion, die mir dann aus Personen ein Integer
macht, weil es zum Beispiel das Alter abfragt, kann mit der gleichen Map-Funktion weiterarbeiten. Ich
muss da also nichts anpassen. Ich muss nur einmal Map programmieren und kann dann beliebig
andere Funktionen da reingeben. Das ist so ein bisschen, wie wir es eben gehört haben, mit den
generischen Klassen. Den generischen Code, den programmiere ich auch nur einmal und dann ändere ich
einfach den Datentyp an der Klasse. Und bei den Funktionen höherer Ordnung ist es so, ich
programmiere auch einmal die Implementierung und dann kann ich das, was die Implementierung
konkret machen soll, von draußen quasi austauschen, indem ich eine Funktion reingebe, die angewendet
werden soll. Dadurch kann ich also sehr schön wiederverwendbare und abstrakte Funktionen
definieren. Ein weiterer Punkt auf der Liste war die Bedarfsauswertung oder auf Englisch
Lazy Evaluation. Was heißt das? Was ist da genau lazy, also faul? Man sagt ja immer, Programmierer
sind faul. Ist auch gut so. Wir wollen uns das Leben möglichst einfach machen. Das hat hier aber
ja nicht direkt etwas mit zu tun. Es geht hier eher darum, wenn ich einen Ausdruck auswerten
möchte, dann soll das erst dann passieren, wenn ich auch wirklich den Wert brauche. Überlegen wir
noch mal kurz, was ein Ausdruck ist. Kennst du hoffentlich noch aus der Programmierung, wenn ich
sowas habe wie A plus B eben für eine Summe. Dieses A plus B ist ein Ausdruck, der ein Ergebnis
produziert. In diesem Fall eben die Summe von A und B. Wenn ich jetzt diesen Ausdruck irgendwo
programmiere, aber nie auf den Wert zugreife, warum soll der Computer dann A plus B überhaupt
ausrechnen? Ich brauche sie eigentlich gar nicht in meinem Programm. Und genau das ist jetzt die
Idee der Lazy Evaluation. Da wird die Auswertung dieser Ausdrücke so lange hinausgezögert, bis der
Wert auch wirklich benutzt wird. Nehmen wir mal ein Beispiel. Angenommen, das würde gehen mit
einer Summe. Der Computer berechnet Summen sofort. Aber angenommen, das würde gehen, dann hätte ich
als A plus B einer Variablen zugeordnet. Und anstatt dass der Computer jetzt sagt, okay, ich
berechne A plus B und weise den Wert der Variablen zu, weist der Computer erstmal nur diese Funktion
A plus B der Variablen zu. Und irgendwann, 27 Zeilencode später, wenn ich dann zum ersten Mal
auf das Ergebnis, also auf die Summe zugreife, weil ich die dann zum Beispiel auswerten will
oder ausgeben will, erst dann wird die Addition wirklich durchgeführt. Also erst dann,
wenn wirklich der Wert benötigt wird, wird die zugrunde liegende Funktion auch ausgeführt. Und
das hat natürlich Vorteile, wenn ich zum Beispiel Funktionen habe, die nur bei Bedarf ausgeführt
werden, weil zum Beispiel noch ein if oder sowas davor ist. Ich will das nur ausführen, wenn ein
bestimmtes Kriterium greift. Dann wäre es ja blöd, wenn es einfach erstmal immer vorberechnet
wird und dann erkannt wird, oh, if soll ich gar nicht machen, habe ich jetzt umsonst gemacht. So
ist ein Beispiel. Oder wenn ich zum Beispiel mit Streams arbeite, die unendlich lang sind,
ja, also eine unendlich lange Zahlenfolge, wenn ich die durch iterieren will, da könnte ein
bisschen Länge dauern, nämlich eben unendlich lang. Und wenn jetzt aber der Stream sagt,
ich mache das erst, wenn es wirklich angefragt ist, dann kann ich trotzdem über diesen Stream
iterieren und zwar nur so lange, wie ich auch weiter Zahlen anfrage. Wenn ich irgendwann sage,
jetzt reicht es mir, ich höre auf, dann hört der Stream halt einfach auf, aber er zählt nicht
erstmal unendlich viele Zahlen auf, um dann nach 1000 aufzuhören zum Beispiel. Das würde ja auch
gar nicht gehen, weil dann würde der Code unendlich lange laufen. Also für solche Sachen
ist Lazy Evaluation sehr gut geeignet. Ich programmiere das erstmal, aber ausgeführt wird
es erst dann, wenn ich wirklich auf den Wert zugreifen will. Dann haben wir noch ein weiteres
wichtiges Prinzip und zwar die Unveränderbarkeit oder auf Englisch, das ist wahrscheinlich eher
verbreitet, Immutability. Was hat es damit auf sich? Die Werte, mit denen wir arbeiten,
sollen nicht mehr verändert werden. Das heißt, ganz klassische, wie wir programmieren lernen,
mit Variablen, die ihre Werte ändern, das wollen wir überhaupt gar nicht haben. Stattdessen erzeugen
wir immer neue Werte und lassen die alten, wie sie sind. Und das gilt auch für Datenstrukturen
wie Listen. Wenn ich einer Liste ein Element hinzufüge, mache ich das nicht in der Liste,
die dann eins länger wird, sondern ich erzeuge eine komplett neue Liste mit dem Inhalt der alten
plus dem neuen Element. Was ist der Vorteil? Wofür brauche ich sowas? Das ist dann sinnvoll,
insbesondere wenn ich parallele Verarbeitung mache. Stellen wir uns vor, wir haben zwei Threads,
die auf einer gemeinsamen Liste arbeiten und der eine Thread löscht da drin rum oder fügt
was hinzu oder wie auch immer und der zweite Thread auch, dann kommen die sich hundertpro
irgendwann in die Quere. Die löschen vielleicht oder versuchen das gleiche Element zu löschen
oder der eine löscht es, der andere will es drin haben oder wie auch immer. Das heißt,
das geht gar nicht. Stattdessen wollen wir unveränderbare Listen haben. Das heißt,
jeder Thread hat quasi seine eigene, ich sag jetzt mal, Kopie der Liste, die unveränderlich ist und
da drin kann er machen, was er will, weil es keine Auswirkung auf den anderen Thread hat,
der auch gerade damit arbeitet. Das lässt sich in funktionalen Sprachen sehr gut umsetzen,
weil die Datenstrukturen entsprechend vorbereitet sind. In Java und anderen Sprachen ist das nicht
ganz so. Also wenn du eine Arrayliste sagst, add, dann fügt die sich selbst ein Element hinzu und
ist danach eins länger. Ja, was wir jetzt aber eigentlich bräuchten ist, erzeug eine neue Liste,
kopiere alle Elemente in die neue Liste und füge das neue Element hinten dran und lasse die alte
Liste unverändert. Da kannst du dir schon vorstellen, dass das ziemlich speicherineffizient
ist, weil du auf einmal die doppelte Speichermenge hast. Du hast zwei Listen im Speicher auf einmal,
die alte und die um eins längere. Das ist natürlich sehr ungünstig, deswegen machen
funktionalen Sprachen das ein bisschen anders. Die arbeiten dann intern mit bestimmten Datenstrukturen
darauf optimiert sind. Die müssen also nicht die ganze Liste rüber kopieren, sondern die haben eine
Art Zeiger auf die alte Liste und die neue Liste ist dann quasi das eine Element plus die alte Liste.
Das geht aber leider in Java nicht. Es gibt allerdings Funktionsbibliotheken, die sowas
erweitern, wie zum Beispiel Waver, so der Standard eigentlich für funktionale Programmierung. Da gibt
es solche unveränderbaren Listen, mit denen du dann arbeiten kannst. Ja, ich guck mal. Oh Gott,
ich muss mal ein bisschen hinmachen hier. Schon fast eine Stunde voll und ich habe noch einen
ganzen Haufen Themen. Machen wir weiter mit Rekursion. Rekursion kennst du bestimmt schon
aus der ganz normalen Programmierung. Eine Funktion, die sich immer wieder selber aufruft.
Du weißt allerdings, dass das in normalen Programmiersprachen nicht so gut ist, weil jede
Funktion immer auf den Stack gelegt wird und irgendwann ist der Stack voll und dann knallt es.
Dann gibt es den berühmten Stack Overflow. Funktionale Programmiersprachen sind soweit
vorbereitet, dass sie quasi aus einer rekursiven Programmierung automatisch eine Iteration machen.
Das heißt, du programmierst einen rekursiven Aufruf in deinem Code, aber der Compiler erkennt,
oh, was er hier eigentlich machen will, ist einfach nur mit dem nächsten Wert quasi nochmal
die Funktion aufrufen und macht daraus quasi automatisch eine Iteration, also eine Schleife
zum Beispiel. Das heißt, es kann nicht zu einem Stack Overflow kommen. Das ist in Java nicht so.
Wenn du einen rekursiven Aufruf machst, ist das ein rekursiver Aufruf, der wird auch nicht
automatisch umgewandelt in irgendwas. Das heißt, in Java funktioniert das so nicht,
aber in anderen Sprachen funktioniert das. Und das Feature, auf das man achten muss,
ist die sogenannte Tail Call Optimization. Das heißt, wenn der allerletzte Befehl in
deiner Methode der rekursive Aufruf der Methode selbst ist, dann kann die Programmiersprache das
in eine Iteration umwandeln und dann entsprechend auch mit, ja, ich will jetzt nicht sagen unendlicher
Rekursion arbeiten, aber auf jeden Fall, du kannst deine Rekursion so benutzen, als wäre es eine ganz
normale Schleife und damit auch bis eine Milliarde zählen und das wird den Computer nicht zum
Absturz bringen. Wenn du das in Java machst, ist nicht nur bei einer Milliarde Schluss,
sondern schon deutlich früher, je nachdem, was dein Speicher so hergibt, aber ich schätze jetzt
einfach mal maximal ein paar tausend rekursive Aufrufe werden funktionieren. Danach ist Schluss,
tendenziell sogar eher weniger, also vielleicht sogar nur ein paar hundert. Also in Java programmieren
wir nicht rekursiv, in der richtigen funktionalen Sprache machen wir das aber, weil das der Standard
ist, um zu iterieren. Jetzt fragt man sich, warum? Warum nämlich einfach Schleifen? Weil Schleifen
Mutable State haben und genau dieser Unveränderbarkeit, der Immutability widersprechen.
Denn wenn du iterierst, brauchst du eigentlich immer eine Iterationsvariable. Nimm die klassische
Vorschleife, da hast du I als Iterationsvariable und was macht die Schleife? Rechnet immer einen
drauf auf I. Das heißt, I wird manipuliert, I wird verändert und das widerspricht der
Immutability. Bei Rekursionen sagst du einfach, so, ich bin jetzt fertig, ruf bitte die gleiche
Methode mit einem neuen, veränderten Parameter auf, aber dieser Parameter wird nicht in der
aktuellen Iteration manipuliert, sondern erzeugt wirklich einen ganz neuen Methodenaufruf mit
neuem Parameter. Das heißt, die aktuelle Iteration wird nicht beeinträchtigt, es gibt
keinen geteilten Zustand, diese Iterationsvariable, sondern jeder Methodenaufruf steht allein für sich
und kriegt seinen eigenen Wert rein und ist damit unveränderbar. Deswegen wird die Rekursion
bevorzugt in der funktionalen Programmierung, weil die ganz normale Iteration mit Schleifen
halt eben nicht Immutable ist. So, letzter Punkt der Prinzipien, referenzielle Transparenz. Was
hat es damit auf sich? Wir haben schon gelernt, wenn wir eine reine Funktion haben, erzeugt die
zu gegebenen Eingangsparametern immer das gleiche Ergebnis. Und das bedeutet jetzt, wenn wir das
übertragen auf den Code, überall, wo ich eine Funktion mit bestimmten Parametern aufrufe,
könnte ich theoretisch auch einfach das Ergebnis dieser Funktion einsetzen. Beispiel, statt zu
schreiben addiere 3,5, könnte ich einfach direkt 8 hinschreiben, weil das Ergebnis wird ja nie anders
sein. Wenn addiere eine pure, eine reine Funktion ist, wird 3 und 5 als Parameter immer 8 ergeben.
Das heißt, ich kann 8 hinschreiben oder addiere 3,5, das ist egal, weil beides gleichwertig ist.
Und das ist die Idee der referenziellen Transparenz. Ein Ausdruck kann jederzeit
durch seinen Wert ersetzt werden, aber auch umgekehrt. Das hat jetzt den Vorteil, dass meine
Programme zum Beispiel sehr viel verständlicher werden. Wenn ich den Ausdruck da sehe, dann weiß
ich, was rauskommen muss. Es kann auch nicht aufgrund irgendeines Zustands was anderes jetzt
rauskommen. Das heißt, es ist sehr viel einfacher nachzuvollziehen, zu welchem Ergebnis meine
Programme kommen, weil es ja eindeutig ist, wenn ich da einen Ausdruck sehe, es kann nicht
abhängig von irgendwelchen Zuständen, irgendwelchen Variablen auf einmal was anderes rauskommen,
als das, was ich erwarte.
So, dann mache ich mal weiter, auch wenn ich jetzt schon bei einer Stunde angekommen bin.
Ich glaube, den Rest, den haue ich jetzt noch durch, weil ich finde das so wichtig.
Das nächste Kapitel, was ich mir hier überlegt habe, ist funktionale Datenstrukturen.
Und da hatte ich ja im Inhaltsverzeichnis des Buchs schon gesagt, Funktoren, Monoide und Monaden gibt es.
Und die gehen wir mal im ganz fixen Schnelldurchlauf einmal durch.
Wir fangen mit den Funktoren an.
Und das sind einfach Datenstrukturen, die die Funktion Map anbieten.
Map habe ich eben schon erklärt, was das macht.
Wir haben ja meist generische Datentypen, wie zum Beispiel eine Liste aus Personen hatten wir gerade.
Und wenn ich diese Liste umwandeln will in eine Liste aus den Vornamen dieser Personen, dann nehme ich dafür die Funktion Map.
Und die Funktion Map ist eine Funktion höherer Ordnung, die kriegt eine andere Funktion rein.
Und diese andere Funktion bekommt genau eine Person rein und gibt einen String zurück.
Die wird also auf ein Element der Liste angewendet.
Die Map-Funktion macht jetzt die Iteration und ruft diese übergebene Funktion für jedes Element der Liste auf und erzeugt daraus dann die Liste der Vornamen zum Beispiel.
Und wenn ich so eine Datenstruktur habe, dann ist das ein sogenannter Funktor.
Das heißt, ein Funktor hat die Map-Funktion, um den enthaltenen Datentypen in einen anderen Datentypen zu transformieren.
Das nennt man Funktor.
Beispiele in Java gibt es mehrere.
Stream hatte ich schon genannt.
Optional aber auch.
Ich kann auch ein Optional, also ein Objekt, was entweder ein Element enthält oder nicht, das kann ich auch mit Map transformieren.
Und wenn der Optional halt leer ist, dann ist er auch im Nachhinein leer.
Aber wenn im Optional was drin war, dann ist danach was drin, was jetzt den neuen Typen hat.
Also das Map-Prinzip kann ich auf verschiedene andere Datenstrukturen übertragen.
Das sind jetzt alles sogenannte Funktoren.
Nächster Punkt auf der Liste sind Monoide.
Monoide.
Damit kann man eine Menge von Elementen zu einem Gesamtergebnis verknüpfen.
Also reduzieren, zu einem Wert reduzieren.
Denken wir mal in SQL an unsere Aggregatsfunktion.
Wenn du sowas machst wie select count from, dann ist das Ergebnis eigentlich eine Tabelle, in der Elemente stehen.
Aber count reduziert diese Liste jetzt auf einen einzigen Wert, nämlich auf ihre Anzahl.
Das gleiche würdest du mit sum zum Beispiel haben oder average oder sowas.
Und das Ganze gibt es jetzt auch in Datenstrukturen.
Das heißt, wenn du so etwas hast wie ich habe eine Liste und ich hätte gerne, bleib mir bei den Personen,
ich hätte gerne das Durchschnittsalter aller Personen.
Dann hast du eine Liste aus Personen, die reingeht und raus kommt ein Double zum Beispiel oder Int oder was auch immer.
Also mehrere Werte rein, ein Wert raus und das Ganze wäre dann ein sogenannter Monoid.
Und die Funktion, die dafür notwendig ist, die heißt in den funktionalen Sprachen eigentlich Fold.
Also sowas wie zusammenklappen, weil das immer so funktioniert.
Das wird elementpaarweise durchgegangen.
Wenn du die Personen von eben dir noch mal als Beispiel nimmst,
diese Funktion, die du jetzt der Fold-Funktion reingibst,
guckt sich immer zwei Elemente der Liste an und ermittelt dann das Ergebnis.
Also angenommen, wir würden die Summe von irgendwas berechnen,
nimmt er die erste Person und die zweite und summiert beide Werte auf.
Mit dem Ergebnis dieser Summe geht er dann in die nächste Person und summiert den Wert auf.
Mit dem Ergebnis geht er wieder in die nächste Person und summiert den auf.
Das heißt, es werden immer zwei Werte miteinander verbunden und dann wieder über die gesamte Liste drüber laufen lassen,
sodass am Ende die Gesamtsumme aller Personen in der Liste rauskommt.
Und dafür ist aber noch eins wichtig und zwar das sogenannte neutrale Element.
Denn angenommen, wir nehmen die Summe, du musst ja mit irgendwas anfangen.
Wenn du mit einer Person loslegst, das ist ja keine Zahl.
Das heißt, du musst erstmal sagen, ich will vielleicht was auch immer das Alter summieren,
auch wenn das nicht ganz so sinnvoll ist.
Mit was summierst du denn die allererste Person in der Liste, wenn du noch nichts hast?
Und genau dafür brauchst du das neutrale Element.
Das ist tatsächlich genau das Ding, was du aus dem Mathematikunterricht noch kennst.
Das neutrale Element der Addition ist die 0.
Du kannst unendlich oft plus 0, plus 0, plus 0 hinter eine Summe schreiben
und der Wert der Summe verändert sich nicht.
Das neutrale Element der Multiplikation ist die 1,
denn du kannst ganz oft machen mal 1, mal 1, mal 1, mal 1, ohne den Wert zu ändern.
Du brauchst jetzt bei dieser Reduktion also ein neutrales Element,
mit dem es losgeht, quasi dein Startwert,
und dann die Funktion, die diesen Startwert bekommt,
und das nächste Element aus der Liste, um dann den neuen,
also weiterführenden Startwert zu berechnen.
Beispiel für die Summe.
Du gehst mit 0 rein und summierst das Alter der ersten Person drauf.
Dann gehst du mit diesem gerade neu ausgerechneten Summe,
mit der Summe aus 0 und dem Alter, in die nächste Person und addierst das Alter auf.
Mit diesem neuen Wert gehst du in die nächste Person und summierst das nächste Alter auf,
und so weiter und so fort.
Das heißt, das neutrale Element ist einfach nur das erste Ding, was summiert wird,
damit du nicht mit nichts startest, sondern quasi mit einem Startwert,
und das muss das neutrale Element sein,
damit du diese Summe nicht, oder diese Operation nicht manipulierst,
sodass sie nicht mehr richtig ist.
Also wenn du zum Beispiel mit 20 anfängst als Startwert,
dann wäre das Durchschnittsalter von einer 10-jährigen Person auf einmal 30.
Das geht ja nicht.
Das heißt, du musst mit 0 oder mit 1 oder was auch immer anfangen.
Auf jeden Fall dem neutralen Element der Operation.
So, und vom Monoid kommen wir jetzt zum letzten, nämlich der Monade.
Und eine Monade, darüber kann man alleine eine Stunde was erzählen.
Habe ich übrigens auch schon mal gemacht, habe ich mal einen Vortrag darüber gehalten.
Ist in den Shownotes alles verlinkt, kannst du mal reingucken.
Die Folien, aber auch Artikelbeweise habe ich dazu mal geschrieben.
Und eine Monade bietet im Prinzip zwei Funktionen an.
Das kann man in Java nicht 1 zu 1 abbilden, aus verschiedenen Gründen.
Das will ich jetzt hier nicht im Detail läutern, kannst du gerne in meinem Artikel nachlesen.
Aber grundsätzlich geht es darum, dass die zwei Funktionen anbietet eine Monade.
Und zwar eine Funktion, um aus einem beliebigen Datentyp eine Monade mit diesem Datentyp zu machen.
Ganz einfaches Beispiel, optional off.
Du hast ein String und du möchtest aus dem String ein optional off String machen.
Dann rufst du auf optional.off und gibst da dein String rein.
Und dann hebt die Monade den Basisdatentypen in den monadischen Datentypen.
Du machst also aus dem String ein optional off String oder ein list off String oder ein stream off String oder was auch immer.
Du hast jetzt also nicht mehr den Datentyp selbst, sondern du hast diese Monade da drumherum jetzt.
Jetzt muss es noch eine weitere Methode geben in dieser Monade und zwar flatMap.
Wenn es die gibt, dann haben wir eine Monade.
Und was macht jetzt das flatMap?
Das Map habe ich schon erklärt. Da wird aus dem inneren Datentyp ein anderer Datentyp gemacht. Fertig.
Ein flatMap ist jetzt eine Funktion oder kriegt eine Funktion rein, die aus diesem inneren Datentyp eine Monade des neuen Datentyps macht.
Das heißt, wenn du vorher einen String da drin hattest, kriegst du mit der Funktion, die du reingibst, jetzt nicht ein Integer,
sondern ein list off Integer zum Beispiel oder ein optional off Integer oder wo auch immer du gerade drin bist.
Angenommen, du hast ein optional off String, also es könnte ein String drin sein oder nicht.
Und wenn du jetzt dessen Länge haben willst, dann brauchst du vielleicht eine Funktion, die aus dem String ein optional off Int macht.
Beim Map würdest du aus dem String ein Int machen.
Beim flatMap würdest du aus dem String ein optional off Int machen.
Das heißt, das Ergebnis ist nicht der Datentyp, sondern die Monade mit diesem enthaltenen Datentyp.
Und was du dann als Ergebnis hättest, wäre ein optional off optional off Int.
Und das wäre irgendwie ungünstig.
Beziehungsweise nicht off Int, übrigens off Integer, weil mit primitiven Datentypen funktionieren die Dinger nicht in Java.
Aber zum Verständnis.
Das heißt, wenn du jetzt aber nicht das optional off optional off Int haben willst, sondern den optional off Int,
dann musst du quasi einen optional wieder flach klopfen.
Und deswegen heißt das flatMap.
Du hast quasi ein optional zu viel und den willst du weghaben.
Deswegen machst du das flatMap.
Ein Beispiel, damit das vielleicht noch etwas verständlicher wird, was mit dem optional gedöhnt.
Das war glaube ich ein bisschen schwer nachzuvollziehen.
Angenommen, du hast eine Liste aus Personen.
Und diese Personen können Kurse belegen in der Uni zum Beispiel.
Und jede Person kann mehrere Kurse belegen.
Also zum Beispiel Englisch und Medizin oder sonst irgendwas.
Das heißt, du hast mehrere Personen.
Und jede Person kann wiederum mehrere Kurse belegen.
Wenn du jetzt diese List of Personen umwandeln willst in eine List of Kurs,
in der alle Kurse aller Personen drin stehen, dann wird es schwierig.
Wenn du mit Map hergehst, machst du aus der Liste der Personen eine Liste of.
Liste of Kurs, weil du zu jeder einzelnen Person mehrere Kurse bekommst.
Also eine List of Kurs.
Und dann hast du als Ergebnis eine Liste, in der Listen von Kurse drin stehen.
Und dieses List of List ist ungünstig.
Wenn du die Kurse alle in einer einzigen Liste haben willst, dann machst du ein flatMap.
Und das klopft diese eine Struktur flach.
Das heißt, anstatt einer Liste aus Listen hast du am Ende eine Liste,
in der alle Elemente der Listen hintereinander einfach drin stehen.
Für sowas brauchst du ein flatMap.
Mit diesen schönen Datenstrukturen, den Funktoren, Monoiden und Monaden,
kann man herrliche Dinge anstellen.
Dazu kannst du am besten auch in dieses Buch schauen und es durchlesen.
Da findest du dann ein paar schöne Beispiele dafür, was man damit machen kann.
Und ein Beispiel habe ich eben gerade genannt.
Um Personen zu Kursen umzuwandeln und aus den Kursen dann nochmal die Namen rauszuholen
und dann die Länge der Namen und so weiter, kannst du so Funktionsketten machen.
Du machst irgendwie student.flatMap auf getKurse.
Dann kannst du das wieder mappen auf den Namen des Kurses
und das kannst du wieder mappen auf die Länge zum Beispiel.
Und so kannst du dir also verschachtelte Strukturen,
also verschachtelt nicht, aber hintereinander gekettete Aufrufe von Funktionen zusammenstöpseln.
Und wenn alle diese Zwischenbestandteile eben Monaden zum Beispiel sind,
verhalten die sich alle gleich und du kannst überall flatMap und Map und so weiter drauf aufrufen.
Und so kommst du dann auch in Richtung dieser funktionaleren Programmierung.
Wahrscheinlich hast du den ersten Anwendungsfeld, wenn du irgendwie Listen verarbeitest.
Anstatt irgendwie eine ForEach-Schleife drumherum zu machen oder sogar eine Vorschleife mit eigenem Zähler
und dann da irgendwie was hin und her zu konvertieren und eine neue Liste zu machen
und da Elemente wieder hinzuzufügen.
Das ist alles mutable ohne Ende.
Du brauchst eine neue Liste, in die du Sachen wieder reinfügst.
Dann wächst die Liste also ständig an.
Das ist ja die Definition der Veränderbarkeit im Prinzip.
Und das kannst du mit funktionalen Datenstrukturen alles machen,
ohne überhaupt selber zu iterieren und selber Listen erzeugen zu müssen.
Statt also zu sagen, ich habe hier eine Liste von Personen und erzeuge eine neue Liste.
Iterier jetzt über die Personen und hol mir aus der Person den Namen
und pack den Namen in diese Liste rein, die ich gerade erzeugt habe.
Das müsstest du ja klassischerweise machen.
Könntest du jetzt in der funktionalen Programmierung einfach sagen,
komm, ListOfStudent.mapStudentGetName und du bist fertig.
Du musst weder die Iteration programmieren, noch eine eigene Liste bauen
oder irgendwas hinzufügen.
Das macht quasi die Sprache automatisch für dich.
Und da siehst du dran, dass es relativ einfacher,
es kommt nur noch darauf an, ob man es gut versteht oder nicht,
auf jeden Fall ist es kürzerer Code
und du hast halt keine veränderbaren Variablen mehr da drin,
die du eventuell falsch programmiert hast, nicht gut verstehen kannst, wie auch immer.
So, kann ich dir nur ans Herz legen, dir das im Detail anzugucken.
Egal, in welcher Sprache du unterwegs bist,
die meisten werden heutzutage funktionale Konzepte anbieten
und ich schreibe dann bei meinen Azubis im Codereview immer dran,
geht das nicht streamiger, wenn ich sowas sehe wie eine Vorschleife oder eine ForEach-Schleife?
Denn das kann man auch in Java schon seit Version 8, also inzwischen schon seit 5 Jahren,
locker machen, indem man Map, FlatMap, Filter, Reduce etc. anwendet.
Das gibt es auch alles schon seit Jahren in Java tatsächlich.
Ja, letzter Punkt für heute und den halte ich jetzt auch wirklich kurz,
da kann ich jetzt nicht mehr im Detail drauf eingehen,
auf die Asynchronität, die wird im Buch auch hervorragend erklärt.
Nur kurz nochmal zur Erläuterung, warum ich sowas brauche heutzutage.
Das Beispiel, was ich vorhin schon mal genannt habe,
wenn ich eine App habe und starte irgendwie ein Download in der App
und der Download dauert 5 Minuten, weil das ein riesen Datei ist,
soll die App in der Zwischenzeit dann nicht mehr reagieren?
Soll die quasi einfrieren, weil auf den Download gewartet wird?
Ich glaube, das wollen wir nicht.
Stattdessen wollen wir, dass der Download im Hintergrund abläuft
und wenn er fertig ist, soll er mich einfach benachrichtigen, dass ich jetzt weitermachen kann.
Und genau das ist das Prinzip der Asynchronität.
Genauso redet man auch von Synchronem und Asynchronem Code.
Wenn du zum Beispiel eine Methode aufrufst, wenn du die synchron aufrufst,
dann wartet die aufrufende Methode darauf, dass die aufgerufene Methode fertig wird.
Und wenn die lange braucht, dann wartet die Methode auch entsprechend lange.
Wenn du asynchron eine Methode aufrufst, dann macht der Aufrufer einfach so weiter,
geht in der nächsten Zeile Code weiter und führt einfach weiter aus
und im Hintergrund wird dann die andere Methode ausgeführt.
Und erst wenn die fertig ist, dann meldet die sich zurück und sagt,
so, jetzt bin ich soweit und dann läuft der Code mit dem Ergebnis weiter.
Aber in der Zwischenzeit, während die erste Methode gewartet hätte bei Synchronaufruf,
kann sie schon andere Dinge weiter tun.
Und das ist insbesondere bei Apps oder bei Websites oder wo auch immer interessant,
denn dann friert die Anwendung halt nicht ein, weil sie wartet,
sondern du kannst weiter klicken, du kannst mit ihr weiter interagieren
und irgendwann, wenn der Rückruf quasi da ist, so, jetzt ist der Download fertig,
dann kann es weitergehen.
Und das ist der Vorteil der asynchronen Programmierung
und das wird natürlich gerade in netzwerkbasierten Anwendungen immer wichtiger,
weil wir da oft Sachen haben, die einfach lange dauern.
Irgendwas von der Website runterladen oder irgendwas hochladen oder was auch immer.
Irgendetwas tun, was langsam ist und in der Zeit soll natürlich meine Anwendung weiter reagieren.
Dafür brauche ich so asynchrone Verarbeitung.
Das wird im Buch sehr schön erklärt, wie das Ganze funktioniert.
Da kriegst du dann, wenn du mal überlegst, wie das aussehen müsste,
dann kannst du von der Methode, die, was auch immer, eine Datei runterlädt,
nicht eine Datei als Rückgabe wert haben, weil dann würdest du ja darauf warten,
sondern du hast wieder irgendeinen Container drumherum.
Am besten eine Monade.
Und das wäre in Java zum Beispiel das Completable Future.
Das heißt, du kriegst nicht eine File zurück,
sondern du kriegst ein Completable Future of File zurück.
Und dieses Future enthält entweder die Datei, wenn der Download schon fertig ist,
oder eben nicht, wenn er noch nicht fertig ist.
Ist nicht das Gleiche wie ein Optional, weil Optional heißt, Wert da oder nicht.
Completable Future ist, der Wert wird irgendwann einmal da sein.
Das ist also ein anderes Konzept, von der Idee her aber das Gleiche.
Du kriegst nicht die Datei selbst zurück, sondern du kriegst diesen Container zurück,
der die Datei irgendwann einmal enthalten wird.
Und jetzt die Frage, was machst du dann mit dem Ding?
Das Ding kannst du dir erstmal in einer Variable zum Beispiel ablegen
und dann erstmal munter weitermachen mit was auch immer du gerade machen wolltest.
Und irgendwann musst du dann aber sagen, so jetzt brauche ich aber die Datei.
Jetzt kann ich nicht mehr weitermachen ohne Datei.
Und dann kannst du zum Beispiel einem Future sowas machen wie, ich weiß nicht wie es heißt,
Get vielleicht, und dann gibt er dir die Datei.
Und wenn sie dann noch nicht runtergeladen ist,
dann wird auch gewartet, bis sie runtergeladen wurde.
Das heißt, das wäre dann ein synchroner Aufruf.
Aber da gibt es dann auch noch verschiedene andere Möglichkeiten,
wie man damit vernünftig programmiert.
Hast du vielleicht schon mal gehört, beim Optional ist es auch ein Anti-Pattern,
sowas zu machen wie, if Optional is present, dann hol den Wert raus.
Sondern da gibt es auch spezielle Funktionen höherer Ordnung an dem Optional,
mit dem du eigentlich arbeiten solltest.
Denn dann, wenn du so mit dem Optional arbeitest,
if is present, dann irgendwas, dann hast du nichts gewonnen.
Weil dann könntest du dir das Optional auch sparen und einfach direkt sagen,
if is null, mach was anderes.
Und wie du eigentlich damit umgehen solltest, ist genau diese Funktionen zu benutzen.
Also zum Beispiel Map.
Statt zu sagen, wenn der Optional gefüllt ist,
dann nimm den Wert und mach damit etwas,
könntest du sagen, Optional, Map, Wert auf anderen Wert.
Und dann entscheidet das Optional, ob es einen Wert hat und dann macht es das Map,
oder er hat keinen Wert und macht halt nichts,
beziehungsweise gibt dem der einen Optional zurück.
Und so arbeitet man eigentlich mit diesen Strukturen.
Das heißt, wenn du jetzt das CompletableFutureOfFile hast und sagst,
hey, nicht lad die Datei runter und gib mir die Datei,
sondern lad die Datei asynchron runter und gib mir ein FutureOfDatei
und dann direkt in der nächsten Zeile machst,
if Future, Get oder sonst irgendwas,
dann hast du nichts gewonnen.
Weil dann ist es trotzdem ein synchroner Aufruf.
Stattdessen musst du die angebotenen Funktionen nutzen.
Und so ein Future hat zum Beispiel eine Funktion andThen.
Das heißt, du könntest sagen, DateiDownload.andThenSaveFileToDisk oder sowas.
Und dann macht das Future automatisch, wenn es die Datei runtergeladen hat,
das Speichern auf die Festplatte.
Und du musst dich nicht drum kümmern.
Du musst weder drauf warten, du musst nicht gucken, wann das Ding endlich fertig ist,
sondern du sagst einfach nur, wenn du irgendwann fertig bist,
dann mach bitte das und das.
Und so gibst du dem Future das schon direkt mit
und musst nicht mal mehr drauf warten, wann es fertig wird,
weil das Future dann schon weiß, was es tun muss, wenn es fertig ist.
Und das ist dann wirklich echter asynchroner Code,
wo du wirklich weiter programmieren kannst und weiter was ausführen kannst,
obwohl der Download noch gar nicht fertig ist.
Und wie das Ganze funktioniert und in der API dann aussieht
und was das für Vor- und Nachteile hat und so,
das wird alles im Buch sehr intensiv beschrieben.
Also von daher, guckt da rein.
Ich habe gesagt, ein ganzes Kapitel nur zum Thema Asynchronität
und dann auch noch zur konkreten Ausprägung der Reactive Streams.
Also wirklich sehr, sehr gut da beschrieben.
Kann ich nur wärmstens ans Herz legen.
Ja, ich hatte noch einen Punkt auf der Liste.
Property-Based Testing.
Das war das Letzte, was im Buch steht,
aber das hatte ich ganz am Anfang schon erklärt,
wie du da deine Testfälle etwas anders aufbaust.
Von daher gehen wir es jetzt mal nicht mehr im Detail durch.
Ich bin schon viel zu lange heute unterwegs,
mit eine Stunde zwanzig sehe ich gerade.
Von daher höre ich an der Stelle hier jetzt einfach mal auf.
Ich habe jetzt meiner Meinung nach so die wichtigsten Grundlagen
rund um die funktionale Programmierung erklärt,
aber auch viele coole, andere wichtige Sachen,
die auch prüfungsrelevant sind.
Und zwar, ich wiederhole nochmal, was wir heute besprochen haben,
Lambda-Ausdrücke, Typ-Inferenz, algebraische Datentypen,
also Produkttypen und Summentypen, Pattern-Matching,
Generics und Co- und Kontravarianz.
Das waren so die allgemeinen Grundlagen,
die man auch ganz unabhängig von der funktionalen Programmierung
durchaus in Sprachen wie Java oder C-sharp kennen
und erklären können sollte als Prüfling.
Dann haben wir die Prinzipien der funktionalen Programmierung,
also sowas wie die klassischen Säulen der Objektorientierung.
Und das wären zumindest die Sachen, die vorher durchgegangen sind.
Funktionsobjekte, also First Class Citizens,
dann reine Funktionen, Pure Functions,
Funktionen höherer Ordnung, das waren die Higher Order Functions,
Bedarfsauswertung, also Lazy Evaluation,
Unveränderbarkeit, die Immutability,
Rekursion als Mittel zur Wiederholung, Recursion,
und referenzielle Transparenz, Referential Transparency.
Das sind so die wichtigen Bausteine der funktionalen Programmierung,
die würde ich von einem 1er-Kandidat auch gerne erläutert haben.
Und danach haben wir uns noch über funktionale Datenstrukturen ausgetauscht.
Funktoren, Monoide, Monaden muss man, glaube ich,
in der Detailtiefe nicht in der mündlichen Prüfung erklären.
Ich glaube, das geht ein bisschen weit.
Aber wenn du einfach besser verstehen willst,
was eigentlich passiert, wenn du da bestimmte Methoden aufrufst,
wie Map und FlapMap und so weiter, was der Sinn dahinter ist,
dann schadet es ja nicht, das Ganze hier als Hintergrundwissen zu kennen.
Und als Letztes, Asynchronität habe ich jetzt etwas kürzer gemacht,
ist aber für die Praxis natürlich absolut relevant,
gerade wenn du zum Beispiel Web-Anwendungen entwickelst mit AJAX oder sowas,
wo du Requests machst, die irgendwie asynchron vom Server zurückkommen.
Das ist ja gang und gäbe heute.
Fast jede Web-Oberfläche bietet oder nutzt sowas.
Von daher solltest du auch verstanden haben, wie das funktioniert.
Insbesondere, wenn du auch so Frameworks benutzt,
wie vielleicht React oder Angular oder so,
da ist ja auch quasi alles automatisch asynchron, was du da machst.
Und da musst du schon etwas anders programmieren,
als du es von deinen klassischen Synchron-Methoden-Aufrufen kennst.
Und deswegen auch hier, Leseempfehlung, guck dir das mal an,
wie man vernünftig asynchron entwickelt.
Das kannst du schön in dem Buch nachlesen.
So, jetzt mach ich aber den Sack zu für heute.
Ich kann nur sagen, kauf dir das Buch, lies es dir durch.
Du wirst auf jeden Fall was mitnehmen,
auch wenn du kein Hardcore-Java-Entwickler bist.
Und auch für Azubis kann ich es wärmstens empfehlen.
Ich hoffe, ich konnte dir heute ein bisschen was über die Funktionale Programmierung näher bringen.
Und hoffe, dass du dir auch das Buch anguckst, weil ich es wirklich empfehlen kann.
Ich fand es wirklich gut.
Ich habe noch einige ergänzende Links, zum Beispiel zu meinen Vorträgen
rund um Themen zur Funktionalen Programmierung,
aber auch ein paar Artikel, wo noch mal Summen und Produkttypen erklärt werden usw.
Schau einfach mal in die Shownotes und die findest du wie immer unter
itberufepodcast.de
slash 158
für die 158. Episode, die das hier heute war.
Und ich würde mich natürlich auch über dein Feedback freuen.
Schreib mir gerne einen Kommentar oder noch besser hinterlass eine kleine Rezension
oder auch einfach nur eine kleine Sternebewertung bei Apple Podcasts
oder wo auch immer du das hier gerade hörst.
Das würde mich sehr freuen und empfiehle mich weiter, wenn es dir weitergeholfen hat.
Ja, damit sage ich für heute erstmal vielen Dank für die Aufmerksamkeit,
die ja heute sehr strapaziert wurde mit einer Stunde zwanzig.
Aber ich hoffe, es war was Interessantes für dich dabei.
Und ich sage Tschüss und bis zum nächsten Mal.










Ich habe diese Podcast-Folge erst die letzten Tage empfohlen bekommen und habe mir diese gleich auf der Autorückfahrt nach Hause in Spotify angehört.
Das Thema ist wahrscheinlich gefühlt zeitlos und daher spielte es auch keine Rolle, dass die Folge Nov. 2020 war. Das Buch habe ich gestern gleich bestellt und freue mich bereits darauf. Vielen Dank für die Empfehlung dazu!
Auch inhaltlich war die Folge vollgespickt mit wichtigen Dingen. Die Kirsche obendrauf war noch die Sprache auf Deutsch.
Die Folge werte gleichzeitig ich als pädagogisch wertvoll, da es in kurzer Zeit viele Aspekte anschneidet; mit einer notwendigen Tiefe, die wiederum erlaubt ein erstes Verständnis zu bekommen (für manche Konzepte kannte ich den en/de Namen nicht). Die Folge eignet sich meiner Meinung sehr in einem Team von Entwicklern als Einstieg oder Basis eines gemeinsamen gedanklichen Modells. Das ist sehr wertvoll.
Daher wollte ich auf jeden Fall dieses Feedback dazu geben. Vielen Dank für die Folge und deinen Podcast, den ich allgemein sehr empfehlenswert finde.
Danke für dein Feedback! Freut mich, wenn ich helfen kann. FP wird immer wichtiger und das genannte Buch finde ich sehr gelungen für einen Einstieg.