Stufe 4Funktional denken · Lektion 23 von 34
Monaden – das Muster hinter flatMap
Das Wort Monade schreckt ab, das Konzept dahinter kennen Sie nach den letzten Lektionen aber bereits: Option, Try, Either, List und Future haben alle ein flatMap, lassen sich in for-Ausdrücken verketten und folgen denselben Regeln. Dieses gemeinsame Muster heißt Monade.
Ein Muster, viele Typen
Schauen Sie auf die vergangenen Lektionen zurück. Jeder der folgenden Typen „verpackt“ Werte auf eine bestimmte Art:
| Typ | Verpackung bedeutet … |
|---|---|
Option[A] | vielleicht ist ein A da, vielleicht nicht |
Try[A] | ein A oder eine Exception |
Either[E, A] | ein A oder ein Fehler vom Typ E |
List[A] | beliebig viele A |
Future[A] | ein A, das später fertig wird |
Und alle haben zwei Dinge gemeinsam:
- Man kann einen einfachen Wert in die Verpackung stecken:
Some(x),Success(x),Right(x),List(x),Future.successful(x). In der Theorie heißt diese Operation unit oder pure. - Man kann eine Funktion anhängen, die aus dem verpackten Wert eine neue Verpackung macht, ohne dass Verpackungen ineinander geschachtelt werden: flatMap.
Ein Typ mit diesen beiden Operationen, die sich an ein paar Regeln halten, ist eine Monade. Mehr steckt nicht dahinter.
Warum flatMap das Herzstück ist
map allein genügt nicht, sobald der nächste Schritt selbst eine Verpackung liefert:
def zahl(s: String): Option[Int] = s.toIntOption
def kehrwert(n: Int): Option[Double] = if n == 0 then None else Some(1.0 / n)
zahl("4").map(kehrwert) // Some(Some(0.25)) – verschachtelt
zahl("4").flatMap(kehrwert) // Some(0.25) – flachflatMap ist map gefolgt von flatten. Es verbindet zwei Schritte, von denen jeder „auf seine Art“ scheitern, mehrfach oder später stattfinden kann – und kümmert sich um die Verpackung, während Sie sich um die eigentliche Logik kümmern.
for-Ausdrücke sind Monaden-Syntax
Wie die Lektion über for-Ausdrücke gezeigt hat, übersetzt der Compiler jeden Generator in flatMap und den letzten in map. Deshalb sieht Code mit ganz verschiedenen Monaden gleich aus:
// Option: kann fehlen
for a <- zahl("4"); b <- kehrwert(a) yield b
// List: alle Kombinationen
for x <- List(1, 2); y <- List("a", "b") yield s"$x$y"
// Either: mit Begründung scheitern
for a <- Right(2); b <- Left("kaputt"); c <- Right(3) yield a + cDie Struktur des Codes ist identisch; was „verketten“ im Einzelnen bedeutet, bestimmt die jeweilige Monade: abbrechen bei None, alle Kombinationen bilden bei List, auf das Ergebnis warten bei Future.
Die drei Gesetze
Damit sich alle Monaden gleich verhalten, müssen sie drei Regeln erfüllen. Mit unit als Verpackungsfunktion (bei Option: Some):
// 1. Linke Identität: verpacken und dann flatMap ist wie direkt anwenden
Some(x).flatMap(f) == f(x)
// 2. Rechte Identität: flatMap mit der Verpackungsfunktion ändert nichts
m.flatMap(Some(_)) == m
// 3. Assoziativität: die Klammerung spielt keine Rolle
m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))Diese Regeln klingen abstrakt, haben aber handfeste Folgen: Sie dürfen Schritte in Hilfsfunktionen auslagern, zusammenfassen und umordnen, ohne dass sich das Ergebnis ändert. Genau darauf verlassen sich Refactorings und for-Ausdrücke.
Eine eigene Monade
Zum Verständnis hilft es, selbst eine zu schreiben. Protokoll trägt neben einem Wert eine Liste von Log-Einträgen mit:
case class Protokoll[A](wert: A, log: List[String]):
def map[B](f: A => B): Protokoll[B] = Protokoll(f(wert), log)
def flatMap[B](f: A => Protokoll[B]): Protokoll[B] =
val naechster = f(wert)
Protokoll(naechster.wert, log ++ naechster.log)
def schritt(n: Int, text: String): Protokoll[Int] = Protokoll(n, List(text))
val ergebnis =
for
a <- schritt(10, "Start mit 10")
b <- schritt(a * 2, "verdoppelt")
c <- schritt(b - 3, "minus 3")
yield c
ergebnis.wert // 17
ergebnis.log // List(Start mit 10, verdoppelt, minus 3)Weil Protokoll map und flatMap hat, funktioniert es sofort im for-Ausdruck. Die Log-Einträge sammeln sich „nebenbei“, ohne dass eine veränderliche Variable im Spiel ist. Bibliotheken nennen dieses Muster Writer-Monade.
Wofür man das Wissen braucht
Im Alltag müssen Sie keine Monaden definieren. Aber das Muster zu erkennen, hilft enorm:
- Sie wissen bei jeder neuen Bibliothek, dass ein Typ mit
flatMapin for-Ausdrücken funktioniert. - Sie verstehen, warum
Future,IOaus Cats Effect oderZIOsich so ähnlich anfühlen. - Sie erkennen, wann zwei Monaden aufeinandertreffen (etwa
Future[Option[A]]) und warum das unhandlich wird – ein Thema, für das Bibliotheken eigene Lösungen anbieten.