scalatutorial.de

Stufe 4Funktional denken · Lektion 23 von 34

Monaden – das Muster hinter flatMap

Aktualisiert am 24. September 20263 Min. LesezeitSchritt 5/6 dieser Stufe

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:

TypVerpackung 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:

  1. 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.
  2. 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:

 Scala 3
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)       – flach

flatMap 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:

 Scala 3
// 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 + c

Die 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):

 Scala 3
// 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:

 Scala 3
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:

Passt dazu