Functional Programming in Pure Java: Functor and Monad Examples

Dieser Artikel war ursprünglich ein Anhang in unserem Buch Reactive Programming with RxJava. Eine Einführung in Monaden, wenn auch sehr im Zusammenhang mit reaktiver Programmierung, passte jedoch nicht sehr gut dazu. Also habe ich beschlossen, es herauszunehmen und dies separat als Blogbeitrag zu veröffentlichen. Ich bin mir bewusst, dass „meine eigene, halb korrekte und halb vollständige Erklärung von Monaden“ das neue „Hallo, Welt“ in Programmierblogs ist. Der Artikel befasst sich jedoch mit Funktoren und Monaden aus einem bestimmten Blickwinkel von Java-Datenstrukturen und -Bibliotheken. Daher dachte ich, es lohnt sich zu teilen.

RxJava wurde auf sehr grundlegenden Konzepten wie Funktoren, Monoiden und Monaden entwickelt und aufgebaut. Obwohl Rx ursprünglich für die imperative C # -Sprache modelliert wurde und wir etwas über RxJava lernen, das auf einer ähnlich imperativen Sprache arbeitet, hat die Bibliothek ihre Wurzeln in der funktionalen Programmierung. Sie sollten sich nicht wundern, wenn Sie feststellen, wie kompakt die RxJava-API ist. Es gibt so ziemlich nur eine Handvoll Kernklassen, die normalerweise unveränderlich sind, und alles besteht hauptsächlich aus reinen Funktionen.

Mit dem jüngsten Aufstieg der funktionalen Programmierung (oder des funktionalen Stils), die am häufigsten in modernen Sprachen wie Scala oder Clojure ausgedrückt wird, wurden Monaden zu einem viel diskutierten Thema. Es gibt viel Folklore um sie herum:

Eine Monade ist ein Monoid in der Kategorie der Endofunktionen, was ist das Problem?

James Iry

Der Fluch der Monade ist, dass man, sobald man die Epiphanie hat, sobald man versteht – „oh, das ist es“ – die Fähigkeit verliert, es jedem zu erklären.

Douglas Crockford

Die überwiegende Mehrheit der Programmierer, insbesondere diejenigen ohne Hintergrund in der funktionalen Programmierung, neigen dazu zu glauben, dass Monaden ein arkanes Informatikkonzept sind, das so theoretisch ist, dass es unmöglich ist Helfen Sie in ihrer Programmierkarriere. Diese negative Perspektive kann auf Dutzende von Artikeln und Blogposts zurückgeführt werden, die entweder zu abstrakt oder zu eng sind. Es stellt sich jedoch heraus, dass Monaden überall um uns herum sind, selbst in einer Standard-Java-Bibliothek, insbesondere seit Java Development Kit (JDK) 8 (dazu später mehr). Was absolut brillant ist, ist, dass, sobald Sie Monaden zum ersten Mal verstehen, plötzlich mehrere unabhängige Klassen und Abstraktionen, die völlig unterschiedlichen Zwecken dienen, vertraut werden.

Monaden verallgemeinern verschiedene scheinbar unabhängige Konzepte, so dass das Erlernen einer weiteren Inkarnation der Monade sehr wenig Zeit in Anspruch nimmt. Zum Beispiel müssen Sie nicht lernen, wie CompletableFuture in Java 8 funktioniert – sobald Sie erkennen, dass es sich um eine Monade handelt, wissen Sie genau, wie sie funktioniert und was Sie von ihrer Semantik erwarten können. Und dann hört man von RxJava, das so viel anders klingt, aber weil Observable eine Monade ist, gibt es nicht viel hinzuzufügen. Es gibt zahlreiche andere Beispiele für Monaden, auf die Sie bereits gestoßen sind, ohne es zu wissen. Daher ist dieser Abschnitt eine nützliche Auffrischung, auch wenn Sie RxJava nicht tatsächlich verwenden.

Funktoren

Bevor wir erklären, was eine Monade ist, wollen wir ein einfacheres Konstrukt namens Funktor untersuchen . Ein Funktor ist eine typisierte Datenstruktur, die einige Werte kapselt. Aus syntaktischer Sicht ist ein Funktor ein Container mit der folgenden API:

import java.util.function.Function;interface Functor<T> { <R> Functor<R> map(Function<T, R> f);}

Aber bloße Syntax reicht nicht aus, um zu verstehen, was ein Funktor ist. Die einzige Operation, die der Funktor bereitstellt, ist map() , die eine Funktion f . Diese Funktion empfängt alles, was sich in einer Box befindet, transformiert es und wickelt das Ergebnis unverändert in einen zweiten Funktor um. Bitte lesen Sie das sorgfältig durch. Functor<T> ist immer ein unveränderlicher Container, daher mutiert map niemals das ursprüngliche Objekt, auf dem es ausgeführt wurde. Stattdessen gibt es das Ergebnis (oder die Ergebnisse – seien Sie geduldig) zurück, das in einen brandneuen Funktor eingeschlossen ist, möglicherweise vom anderen Typ R. Zusätzlich sollten Funktoren keine Aktionen ausführen, wenn die Identitätsfunktion angewendet wird, dh map(x -> x) . Ein solches Muster sollte immer entweder denselben Funktor oder eine gleiche Instanz zurückgeben.

Der Funktor<T> wird mit einer Box verglichen, die eine Instanz von T enthält, wobei die einzige Möglichkeit, mit diesem Wert zu interagieren, darin besteht, ihn zu transformieren. Es gibt jedoch keine idiomatische Möglichkeit, den Funktor zu entpacken oder zu entkommen. Die Werte bleiben immer im Kontext eines Funktors. Warum sind Funktoren nützlich? Sie verallgemeinern mehrere gängige Redewendungen wie Sammlungen, Versprechen, Optionen usw. mit einer einzigen, einheitlichen API, die über alle hinweg funktioniert. Lassen Sie mich ein paar Funktoren vorstellen, um Sie mit dieser API flüssiger zu machen:

interface Functor<T,F extends Functor<?,?>> { <R> F map(Function<T,R> f);}class Identity<T> implements Functor<T,Identity<?>> { private final T value; Identity(T value) { this.value = value; } public <R> Identity<R> map(Function<T,R> f) { final R result = f.apply(value); return new Identity<>(result); }}

Ein zusätzlicher Parameter vom Typ F war erforderlich, um die Identität zu kompilieren. Was Sie im vorherigen Beispiel gesehen haben, war der einfachste Funktor, der nur einen Wert enthielt. Alles, was Sie mit diesem Wert tun können, ist ihn innerhalb der Map-Methode zu transformieren, aber es gibt keine Möglichkeit, ihn zu extrahieren. Dies wird über den Rahmen eines reinen Funktors hinaus betrachtet. Die einzige Möglichkeit, mit functor zu interagieren, besteht darin, Sequenzen typsicherer Transformationen anzuwenden:

Identity<String> idString = new Identity<>("abc");Identity<Integer> idInt = idString.map(String::length);

Oder fließend, genau wie Sie Funktionen komponieren:

Identity<byte> idBytes = new Identity<>(customer) .map(Customer::getAddress) .map(Address::street) .map((String s) -> s.substring(0, 3)) .map(String::toLowerCase) .map(String::getBytes);

Aus dieser Perspektive unterscheidet sich die Zuordnung über einen Funktor nicht wesentlich vom Aufrufen verketteter Funktionen:

byte bytes = customer .getAddress() .street() .substring(0, 3) .toLowerCase() .getBytes();

Warum sollten Sie sich überhaupt mit einem so ausführlichen Wrapping beschäftigen, das nicht nur keinen Mehrwert bietet, sondern auch nicht in der Lage ist, den Inhalt zurück zu extrahieren? Nun, es stellt sich heraus, dass Sie mit dieser rohen Funktorabstraktion mehrere andere Konzepte modellieren können. Zum Beispiel gibt es ab Java 8 Optional einen Funktor mit der map() -Methode. Lassen Sie es uns von Grund auf neu implementieren:

class FOptional<T> implements Functor<T,FOptional<?>> { private final T valueOrNull; private FOptional(T valueOrNull) { this.valueOrNull = valueOrNull; } public <R> FOptional<R> map(Function<T,R> f) { if (valueOrNull == null) return empty(); else return of(f.apply(valueOrNull)); } public static <T> FOptional<T> of(T a) { return new FOptional<T>(a); } public static <T> FOptional<T> empty() { return new FOptional<T>(null); }}

Jetzt wird es interessant. Ein FOptional<T> Funktor kann einen Wert enthalten, aber genauso gut kann er leer sein. Es ist eine typsichere Methode zum Codieren von null . Es gibt zwei Möglichkeiten, FOptional zu konstruieren – indem Sie einen Wert angeben oder eine empty() Instanz erstellen. In beiden Fällen ist FOptional wie bei Identity unveränderlich und wir können nur von innen mit dem Wert interagieren. WasFOptional unterscheidet, ist, dass die Transformationsfunktion f auf keinen Wert angewendet werden darf, wenn er leer ist. Dies bedeutet, dass der Funktor nicht unbedingt genau einen Wert vom Typ T kapselt. Es kann genauso gut eine beliebige Anzahl von Werten umschließen, genau wie List … funktor:

import com.google.common.collect.ImmutableList;class FList<T> implements Functor<T, FList<?>> { private final ImmutableList<T> list; FList(Iterable<T> value) { this.list = ImmutableList.copyOf(value); } @Override public <R> FList<?> map(Function<T, R> f) { ArrayList<R> result = new ArrayList<R>(list.size()); for (T t : list) { result.add(f.apply(t)); } return new FList<>(result); }}

Die API bleibt gleich: sie nehmen einen Funktor in einer Transformation – aber das Verhalten ist viel anders. Jetzt wenden wir eine Transformation auf jedes Element in der FList an und transformieren deklarativ die gesamte Liste. Wenn Sie also eine Liste von Kunden haben und eine Liste ihrer Straßen wünschen, ist dies so einfach wie:

import static java.util.Arrays.asList;FList<Customer> customers = new FList<>(asList(cust1, cust2));FList<String> streets = customers .map(Customer::getAddress) .map(Address::street);

Es ist nicht mehr so einfach, customers.getAddress().street() zu sagen, Sie könnengetAddress() nicht für eine Sammlung von Kunden aufrufen, Sie müssen getAddress() für jeden einzelnen Kunden aufrufen und ihn dann wieder in eine Sammlung einfügen. Übrigens fand Groovy dieses Muster so häufig, dass es tatsächlich einen Syntaxzucker dafür hat: customer*.getAddress()*.street() . Dieser Operator, bekannt als Spread-dot, ist eigentlich ein map in Verkleidung. Vielleicht fragen Sie sich, warum ich list manuell in map iteriere, anstatt Streams von Java 8:list.stream().map(f).collect(toList()) zu verwenden? Läutet das eine Glocke? Was wäre, wenn ich Ihnen sagen würde, dassjava.util.stream.Stream<T> in Java auch ein Funktor ist? Und übrigens auch eine Monade?
Jetzt sollten Sie die ersten Vorteile von Funktoren sehen – sie abstrahieren die interne Darstellung und bieten eine konsistente, einfach zu verwendende API über verschiedene Datenstrukturen. Als letztes Beispiel möchte ich den Promise-Funktor vorstellen, ähnlich wie Future . Promise „verspricht“, dass ein Wert eines Tages verfügbar wird. Es ist noch nicht da, vielleicht weil eine Hintergrundberechnung erstellt wurde oder wir auf ein externes Ereignis warten. Aber es wird irgendwann in der Zukunft erscheinen. Die Mechanik des Abschlusses von aPromise<T> ist nicht interessant, aber die Funktornatur ist:

Promise<Customer> customer = //...Promise<byte> bytes = customer .map(Customer::getAddress) .map(Address::street) .map((String s) -> s.substring(0, 3)) .map(String::toLowerCase) .map(String::getBytes);

Kommt dir das bekannt vor? Darum geht es! Die Implementierung des Funktors geht über den Rahmen dieses Artikels hinaus und ist nicht einmal wichtig. Genug zu sagen, dass wir der Implementierung von CompletableFuture aus Java 8 sehr nahe stehen und Observable aus RxJava fast entdeckt haben. Aber zurück zu den Funktoren. Promise<Customer> enthält noch keinen Wert von Customer . Es verspricht, in Zukunft einen solchen Wert zu haben. Aber wir können immer noch einen solchen Funktor zuordnen, genau wie bei FOptional und FList – die Syntax und Semantik sind genau gleich. Das Verhalten folgt dem, was der Funktor darstellt. Kunden aufrufen.map(Customer::getAddress) liefert die < -Adresse> , was bedeutet, dass map nicht blockiert. Kunde.map() wird Kunde versprechen zu vervollständigen. Stattdessen gibt es ein anderes Versprechen eines anderen Typs zurück. Wenn upstream promise abgeschlossen ist, wendet Downstream promise eine an map() übergebene Funktion an und übergibt das Ergebnis an downstream. Plötzlich können wir mit unserem Funktor asynchrone Berechnungen nicht blockierend ausführen. Aber Sie müssen das nicht verstehen oder lernen – weil Promise ein Funktor ist, muss es Syntax und Gesetzen folgen.

Es gibt viele andere großartige Beispiele für Funktoren, die beispielsweise Wert oder Fehler kompositorisch darstellen. Aber es ist höchste Zeit, Monaden zu betrachten.

Von Funktoren zu Monaden

Ich nehme an, Sie verstehen, wie Funktoren funktionieren und warum sie eine nützliche Abstraktion sind. Aber Funktoren sind nicht so universell, wie man erwarten könnte. Was passiert, wenn Ihre Transformationsfunktion (die als Argument an map() übergebene) eine Funktorinstanz anstelle eines einfachen Werts zurückgibt? Nun, ein Funktor ist auch nur ein Wert, also passiert nichts Schlimmes. Was auch immer zurückgegeben wurde, wird wieder in einen Funktor eingefügt, sodass sich alles konsistent verhält. Stellen Sie sich jedoch vor, Sie haben diese praktische Methode zum Analysieren von Zeichenfolgen:

FOptional<Integer> tryParse(String s) { try { final int i = Integer.parseInt(s); return FOptional.of(i); } catch (NumberFormatException e) { return FOptional.empty(); }}

Ausnahmen sind Nebenwirkungen, die das Typsystem und die funktionelle Reinheit untergraben. In reinen funktionalen Sprachen gibt es keinen Platz für Ausnahmen. Schließlich haben wir noch nie davon gehört, während des Mathematikunterrichts Ausnahmen auszulösen, oder? Fehler und unzulässige Bedingungen werden explizit mit Werten und Wrappern dargestellt. Zum Beispiel nimmt TryParse() einen String, gibt aber nicht einfach ein int zurück oder löst zur Laufzeit stillschweigend eine Ausnahme aus. Wir sagen explizit durch das Typsystem, dass TryParse () fehlschlagen kann, es gibt nichts Außergewöhnliches oder Fehlerhaftes an einer fehlerhaften Zeichenfolge. Dieser Halbfehler wird durch ein optionales Ergebnis dargestellt. Interessanterweise hat Java Ausnahmen überprüft, die deklariert und behandelt werden müssen. Aber zum Guten oder Schlechten werden überprüfte Ausnahmen in Java oft nicht empfohlen, also kehren wir zu TryParse() zurück. Es scheint nützlich zu sein, TryParse mit einer Zeichenfolge zu erstellen, die bereits in FOptional:

FOptional<String> str = FOptional.of("42");FOptional<FOptional<Integer>> num = str.map(this::tryParse);

Das sollte nicht überraschen. Wenn tryParse() ein int zurückgeben würde, würden SieFOptional<Integer> num , aber weil map() Funktion FOptional<Integer> selbst zurückgibt, wird es zweimal in ein FOptional<FOptional<Integer>> . Bitte schauen Sie sich die Typen genau an, Sie müssen verstehen, warum wir diesen Doppelwickler hier haben. Abgesehen davon, dass es schrecklich aussieht, ruiniert ein Funktor in Functor die Komposition und die fließende Verkettung:

FOptional<Integer> num1 = //...FOptional<FOptional<Integer>> num2 = //...FOptional<Date> date1 = num1.map(t -> new Date(t));//doesn't compile!FOptional<Date> date2 = num2.map(t -> new Date(t));

Hier versuchen wir, den Inhalt von FOptional abzubilden, indem wir int in +Date+ . Mit einer Funktion von int -> Date können wir leicht von Functor<Integer> zu Functor<Date> transformieren, wir wissen, wie es funktioniert. Aber im Falle von num2 wird die Situation kompliziert. Was num2.map()als Eingabe erhält, ist nicht mehr ein int, sondern ein FOoption<Integer> und offensichtlich hatjava.util.Date keinen solchen Konstruktor. Wir haben unseren Funktor gebrochen, indem wir ihn doppelt verpackt haben. Eine Funktion, die einen Funktor anstelle eines einfachen Werts zurückgibt, ist jedoch so häufig (wietryParse() ), dass wir diese Anforderung nicht einfach ignorieren können. Ein Ansatz besteht darin, eine spezielle parameterlose join() -Methode einzuführen, die verschachtelte Funktoren „abflacht:

FOptional<Integer> num3 = num2.join()

Es funktioniert, aber weil dieses Muster so häufig ist, wurde eine spezielle Methode namens flatMap() eingeführt. flatMap() ist map sehr ähnlich, erwartet jedoch, dass die als Argument empfangene Funktion einen Funktor oder eine Monade zurückgibt, um genau zu sein:

interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> { M flatMap(Function<T,M> f);}

Wir sind einfach zu dem Schluss gekommen, dass flatMap nur ein syntaktischer Zucker ist, um eine bessere Komposition zu ermöglichen. Aber dieflatMap -Methode (oft bind oder >>= von Haskell genannt) macht den Unterschied, da komplexe Transformationen in einem reinen, funktionalen Stil komponiert werden können. Wenn FOptional eine Instanz von monad , funktioniert das Parsen plötzlich wie erwartet:

FOptional<String> num = FOptional.of("42");FOptional<Integer> answer = num.flatMap(this::tryParse);

Monaden müssen map nicht implementieren, sie können problemlos über flatMap() implementiert werden. Tatsächlich ist flatMap der wesentliche Operator, der ein völlig neues Universum von Transformationen ermöglicht. Offensichtlich reicht die syntaktische Konformität wie bei Funktoren nicht aus, um eine Klasse als Monade aufzurufen, der Operator flatMap() muss Monadengesetzen folgen, aber sie sind ziemlich intuitiv wie die Assoziativität von flatMap() und die Identität. Letzteres erfordert, dass m(x).flatMap(f) für jede Monade, die einen Wert x und eine Funktion f enthält, mitf(x) identisch ist. Wir werden nicht zu tief in die Monadentheorie eintauchen, sondern uns auf praktische Implikationen konzentrieren. Monaden leuchten, wenn ihre interne Struktur nicht trivial ist, zum Beispiel Promise Monade, die in Zukunft einen Wert haben wird. Können Sie anhand des Typsystems erraten, wie sich Promise im folgenden Programm verhält? Erstens geben alle Methoden, deren Abschluss möglicherweise einige Zeit in Anspruch nehmen kann, a zurück Promise:

import java.time.DayOfWeek;Promise<Customer> loadCustomer(int id) { //...}Promise<Basket> readBasket(Customer customer) { //...}Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) { //...}

Wir können diese Funktionen jetzt so zusammenstellen, als würden sie alle mit monadischen Operatoren blockiert:

Promise<BigDecimal> discount = loadCustomer(42) .flatMap(this::readBasket) .flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));

Das wird interessant. flatMap() muss den monadischen Typ beibehalten, daher sind alle Zwischenobjekte Promises. Es geht nicht nur darum, die Typen in Ordnung zu halten – das Programm ist plötzlich vollständig asynchron! loadCustomer() gibt ein Promise zurück, sodass es nicht blockiert. readBasket() nimmt alles, was das Promise hat (haben wird) und wendet eine Funktion an, die ein anderes Promise zurückgibt und so weiter und so fort. Grundsätzlich haben wir eine asynchrone Berechnungspipeline erstellt, bei der der Abschluss eines Schritts im Hintergrund automatisch den nächsten Schritt auslöst.

Es ist sehr üblich, zwei Monaden zu haben und den Wert, den sie zusammenschließen, zu kombinieren. Sowohl Funktoren als auch Monaden erlauben jedoch keinen direkten Zugriff auf ihre Interna, was unrein wäre. Stattdessen müssen wir Transformation sorgfältig anwenden, ohne der Monade zu entkommen. Stellen Sie sich vor, Sie haben zwei Monaden und möchten sie kombinieren:

import java.time.LocalDate;import java.time.Month;Monad<Month> month = //...Monad<Integer> dayOfMonth = //...Monad<LocalDate> date = month.flatMap((Month m) -> dayOfMonth .map((int d) -> LocalDate.of(2016, m, d)));

Bitte nehmen Sie sich Zeit, um den vorhergehenden Pseudocode zu studieren. Ich verwende keine echte Monadenimplementierung wie Promise oder List, um das Kernkonzept hervorzuheben. Wir haben zwei unabhängige Monaden, eine vom Typ Month und die andere vom Typ Integer. Um LocalDate daraus zu erstellen, müssen wir eine verschachtelte Transformation erstellen, die Zugriff auf die Interna beider Monaden hat. Arbeiten Sie die Typen durch und stellen Sie insbesondere sicher, dass Sie verstehen, warum wir flatMap an einer Stelle undmap() an der anderen Stelle verwenden. Überlegen Sie, wie Sie diesen Code strukturieren würden, wenn Sie auch einen dritten Monad<Year> hätten. Dieses Muster der Anwendung einer Funktion mit zwei Argumenten (in unserem Fallm und d) ist so häufig, dass es in Haskell eine spezielle Hilfsfunktion namens liftM2 gibt, die genau diese Transformation ausführt und über map und flatMap implementiert ist. In der Java-Pseudosyntax würde es ungefähr so aussehen:

Monad<R> liftM2(Monad<T1> t1, Monad<T2> t2, BiFunction<T1, T2, R> fun) { return t1.flatMap((T1 tv1) -> t2.map((T2 tv2) -> fun.apply(tv1, tv2)) );}

Sie müssen diese Methode nicht für jede Monade implementieren, flatMap() reicht aus, außerdem funktioniert sie konsistent für alle Monaden. liftM2 ist äußerst nützlich, wenn man bedenkt, wie es mit verschiedenen Monaden verwendet werden kann. Zum Beispiel wendet listM2(list1, list2, function) function auf jedes mögliche Elementpaar von list1 und list2 (kartesisches Produkt) an. Auf der anderen Seite wird für Optionals eine Funktion nur angewendet, wenn beide Optionals nicht leer sind. Noch besser ist, dass für eine Promise -Monade eine Funktion asynchron ausgeführt wird, wenn beide Promise abgeschlossen sind. Dies bedeutet, dass wir gerade einen einfachen Synchronisationsmechanismus (join() in Fork-Join-Algorithmen) von zwei asynchronen Schritten erfunden haben.

Ein weiterer nützlicher Operator, den wir leicht auf flatMap() aufbauen können, ist filter(Predicate<T>), der alles, was sich in einer Monade befindet, aufnimmt und vollständig verwirft, wenn er ein bestimmtes Prädikat nicht erfüllt. In gewisser Weise ähnelt es map, aber anstelle der 1-zu-1-Zuordnung haben wir 1-zu-0-oder-1. Wiederum hat filter()für jede Monade die gleiche Semantik, aber eine ziemlich erstaunliche Funktionalität, je nachdem, welche Monade wir tatsächlich verwenden. Offensichtlich können bestimmte Elemente aus einer Liste herausgefiltert werden:

FList<Customer> vips = customers.filter(c -> c.totalOrders > 1_000);

Aber es funktioniert genauso gut zB für Optionals. In diesem Fall können wir nicht leeres optionales in ein leeres umwandeln, wenn der Inhalt des optionalen einige Kriterien nicht erfüllt. Leere Optionen bleiben intakt.

Von der Liste der Monaden zur Monade der Liste

Ein weiterer nützlicher Operator, der von flatMap() stammt, ist sequence(). Sie können leicht erraten, was es tut, indem Sie einfach die Typensignatur betrachten:

Monad<Iterable<T>> sequence(Iterable<Monad<T>> monads)

Oft haben wir eine Reihe von Monaden desselben Typs und möchten eine einzelne Monade einer Liste dieses Typs haben. Das mag für Sie abstrakt klingen, ist aber beeindruckend nützlich. Stellen Sie sich vor, Sie möchten einige Kunden gleichzeitig nach ID aus der Datenbank laden, sodass Sie die Methode loadCustomer(id) mehrmals für verschiedene IDs verwendet haben, wobei jeder Aufruf Promise<Customer> zurückgibt. Jetzt haben Sie eine Liste von Promise s, aber was Sie wirklich wollen, ist eine Liste von Kunden, die z. B. im Webbrowser angezeigt werden sollen. Der Operator sequence() (in RxJava sequence() heißt je nach Anwendungsfall concat() oder merge()) ist genau dafür gebaut:

FList<Promise<Customer>> custPromises = FList .of(1, 2, 3) .map(database::loadCustomer);Promise<FList<Customer>> customers = custPromises.sequence();customers.map((FList<Customer> c) -> ...);

Mit einem FList<Integer> , der Kunden-IDs darstellt, haben wir map darüber (sehen Sie, wie es hilft, dass FList ein Funktor ist?) durch Aufruf von database.loadCustomer(id) für jede ID. Dies führt zu einer ziemlich unbequemen Liste von Promises. sequence() rettet den Tag, aber auch dies ist nicht nur ein syntaktischer Zucker. Der vorhergehende Code ist vollständig nicht blockierend. Für verschiedene Arten von Monaden macht sequence() immer noch Sinn, aber in einem anderen Rechenkontext. Zum Beispiel kann es FList<FOptional<T>> in FOptional<FList<T>> ändern. Übrigens können Siesequence() (genau wie map() ) über flatMap() implementieren.

Dies ist nur die Spitze des Eisbergs, wenn es um die Nützlichkeit von flatMap() und Monaden im Allgemeinen geht. Obwohl Monaden aus einer eher obskuren Kategorietheorie stammen, erwiesen sie sich selbst in objektorientierten Programmiersprachen wie Java als äußerst nützliche Abstraktion. Die Möglichkeit, Funktionen zu komponieren, die Monaden zurückgeben, ist so universell hilfreich, dass Dutzende von nicht verwandten Klassen monadischem Verhalten folgen.

Sobald Sie Daten in einer Monade kapseln, ist es oft schwierig, sie explizit herauszuholen. Eine solche Operation ist nicht Teil des Monadenverhaltens und führt häufig zu nicht idiomatischem Code. Zum Beispiel kann Promise.get() auf Promise<T> technisch T zurückgeben, aber nur durch Blockieren, während alle Operatoren, die auf flatMap() basieren, nicht blockieren. Ein anderes Beispiel ist FOptional.get() , aber das kann fehlschlagen, weil FOptional leer sein kann. Sogar FList.get(idx), das ein bestimmtes Element aus einer Liste hervorhebt, klingt umständlich, da Sie for -Schleifen häufig durch map() ersetzen können.

Ich hoffe, Sie verstehen jetzt, warum Monaden heutzutage so beliebt sind. Selbst in einer objektorientierten (-ish) Sprache wie Java sind sie eine ziemlich nützliche Abstraktion.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.