Sonntag, 20. März 2011

Monaden in Scala

Am vergangenen Wochenende habe ich beim MATHEMA Campus 2011 einen Vortrag über Monaden in Scala gehalten (Folien hier).

Monaden sind ein etwas sperriges Konzept, das in rein funktionalen Sprachen wie Haskell u.a. für eine vernünftige Ein-/Ausgabeprogrammierung notwendig ist. Für Scala sind Monaden vielleicht nicht ganz so wichtig, aber auch mit Scala kann man sinnvoll monadisch programmieren – und das Schöne daran: Über die ganze komplizierte mathematische Theorie dahinter muss man sich keine Gedanken machen. Im Gegenteil, wer schonmal ein bisschen Scala programmiert hat, wird dabei vermutlich (unbewusst) auch Monaden genutzt haben.

Was sind diese Monaden? Ein abstrakter Datentyp für verkettbare Berechnungen mit folgenden Eigenschaften:
  • Ein Typkonstruktor wie List[A] (mit z.B. List[Int] als Ausprägung).
  • Eine Funktion, um einen Wert im Typ "einzuwickeln". Das kann der Konstruktor sein, z.B. new List(1) bzw. List(1), aber häufig wird diese Funktion return, unit, lift oder pure genannt.
  • Eine Funktion, um die enthaltenen Werte zu verrechnen, aber nach der Berechnung im Typ verpackt zu lassen. Übliche Namen sind bind oder (in Scala) flatMap.
Die Monadengesetze müssen von den Funktionen eingehalten werden, aber das lassen wir mal außen vor. Klingt kompliziert? Schauen wir uns das in der Praxis an:
case class MiniMonade[A](wert:A) {

def flatMap[B](f: A => MiniMonade[B]) = f(wert)

def map[B](f: A => B): MiniMonade[B]
= MiniMonade(f(wert))
}
Die Mini-Monade soll also genau einen Wert des generischen Typs "A" speichern und verarbeiten können. Listen sind auch Monaden und können entsprechend beliebig viele Werte verarbeiten. In unserer Monade ist zusätzlich noch die Funktion map implementiert, damit wir Objekte dieser Klasse in den for-Comprehensions von Scala einsetzen können. Und genau das ist auch der häufigste Anwendungsfall für Monaden in Scala:
val m1 = MiniMonade(2)
val m2 = MiniMonade(3)

val erg = for { x <- m1; y <- m2 } yield x*y;

println( erg )
In dieser for-Comprehension werden alle Werte aus den beiden Monaden-Objekten miteinander mit x*y verrechnet und landen anschließend (durch die Magie der Monaden-Funktionen) wieder in einem Monaden-Objekt. Da unsere Objekte jeweils nur einen Wert speichern, wird nur 2*3 berechnet:
HeartOfGold:~ much$ scala MiniMonade.scala
MiniMonade(6)
Das scheint unnötig viel Aufwand für eine simple Multiplikation zu sein... Aber folgendes Beispiel dürfte – so oder in ähnlicher Form – jeder schonmal programmiert haben, und das ergibt sicher mehr Sinn:
val zeilen = List(10,20)
val spalten = List(1,2)

val summen = for {z <- zeilen; s <- spalten} yield z+s;

println( summen )
// List(11, 12, 21, 22)
Andere gebräuchliche monadische Typen sind (neben vielen weiteren) Map und Option, und alle diese Klassen können in einer for-Comprehension kombiniert werden.

Die Verwendbarkeit in for-Comprehensions ist in Scala sicherlich der wichtigste Grund, warum man geeignete eigene Klassen monadisch machen sollte. Würden wir noch withFilter und foreach implementieren, könnten wir unsere Monade außerdem in weiteren for-Varianten einsetzen.

Beim diesjährigen Herbstcampus werde ich einen Einsteiger-Vortrag (OO-Vorkenntnisse reichen) zu diesem Thema halten.