frosch03.de/posts/2013-03-21-Monaden
Was ist eine Monade?
Kurz gesagt: Eine Monade ist ein Datentyp, der ganz bestimmten Bausteinen enthält und dabei ebenso bestimmte Regeln befolgt.
Genauer: Unter einer Monade versteht man einen Datentyp. Zu diesem
Datentyp gehören die Operationen bind
und return
. Außerdem muss
dieses Konstrukt auch noch die Monaden-Gesetze befolgen.
Diese sind nicht weiter dramatisch. Die Monaden-Gesetzte fordern die linke und die rechte Identität sowie die Assoziativität.
Aber fangen wir mit den Operatoren an. bind
bezeichnet den Operator,
der zwei Monaden zu einer neuen verknüpft; return
nennt man jenen, der
eine Monade als Rückgabetyp zurück gibt.
Werfen wir einen Blick auf die Typen dieser Operatoren:
{% highlight haskell %} return :: a -> m a bind :: m a -> (a -> m b) -> m b {% endhighlight %}
Dem Operator return
kann man also einen Wert (vom Typen a
) geben und
return
liefert einem dann eine Monade zurück, in der irgendwie das
übergebene a
drin steckt.
Mit dem Operator bind
kann man zwei Monaden zu einer neuen verknüpfen.
Dabei enthält die erste Monade etwas vom Typen a
und die zweite Monade
enthält etwas vom Typen b
. Der Rückgabewert des bind
-Operators ist
selber wieder eine Monade die etwas vom Typen b
enthält.
Selbstverständlich können a
und b
auch den gleichen Typen meinen. Um
aber allgemein genug zu sein, werden hier zwei unterschiedliche
Typvariablen verwendet. Ebenfalls um allgemein zu bleiben ist auch der
Teil (a -> m b)
in der Typsignatur gewählt worden. (a -> m b)
kann
auch einfach nur m b
bedeuten.
Halt, nicht so schnell?
Ok, spulen wir noch einmal zurück und vergleichen das mit etwas, das jeder bestimmt kennt.
Angenommen in unserer Welt kommen nur reelle Zahlen vor, also jene
Zahlen, die mit einem Komma dargestellt werden (z.B.: 4,2 oder 3,1415
oder 1,0). Solche Zahlen nennt man oft Float
. Dann stellen wir uns
einen Datentypen vor, der nur ganze Zahlen speichern kann, wir nennen
diesen Integer, kurz Int
(bekannt, oder? :))
Man kann hier jetzt einen Monade entdecken, wenn man ganz leise ist und
genau hinschaut. Nein, aber im Ernst, hier haben wir schon einen Monade,
wenn wir zu dem Integer noch die beiden oben genannten Operatoren
finden. Was könnte das bind
und was könnte das return
auf Integern
sein?
Um aus einer reellen Zahl (die mit dem Komma) eine ganze Zahl zu
erzeugen, kann man die reelle Zahl einfach runden. Wir schreiben also
einen Funktion round :: Float -> Int
, also eine, die aus einer reellen
Zahl eine ganze Zahl erzeugt. Diese Funktion werden wir als die return
Funktion der Monade verwenden.
Dann brauchen wir noch einen Funktion, die als bind
agiert. bind
war
die Funktion, die aus zwei monadischen Werten einen neuen monadischen
Wert erzeugt. Da wir ganze Zahlen als monadische Werte betrachten, kann
jede Funktion, die aus zwei Int's einen neuen Int erzeugt, als bind
Funktion betrachtet werden. Konkret werden wir hier die Addition
(+) :: Int -> Int -> Int
als bind
verwenden.
Wir haben also bis hierher: * Datentyp: Int
* Return:
round :: Float -> Int
* Bind: (+) :: Int -> Int -> Int
Aber was hat das nun mit diesen komischen Typen (wie z.B. (a -> m b)
)
auf sich?
Die Buchstaben a
und b
, sind Typvariablen und zeigen an, dass die
Definition einer Monade ganz allgemein, also für viele verschiedene
Typen gilt. In unserem Fall sind a
und b
vom selben Typ, so dass für
jedes b
auch ein a
geschrieben werden kann. Das m
vor der
Typvariable zeigt an, dass hier ein Typkonstruktor eingesetzt werden
kann (genauso wie für a
ein Datentyp eingesetzt werden kann).
Da wir Int
als monadisch betrachten, setzten wir für m a
jeweils
unseren Int
ein. Wenn wir uns jetzt nochmal an die Definition von
bind
und return
weiter oben erinnern und alle b
durch a
ersetzten, so haben wir:
{% highlight haskell %} return :: a -> m a bind :: m a -> (a -> m a) -> m a {% endhighlight %}
Da in unserem Beispiel aber der allgemeine Fall m a
konkret für Int
steht und der allgemeine Fall a
unseres Beispieles konkret für Float
steht, lassen sich die Definitionen umformulieren in:
{% highlight haskell %} return :: Float -> Int bind :: Int -> (Float -> Int) -> Int {% endhighlight %}
Mit return
haben wir schon exakt den Typ der round
-Funktion
getroffen, lediglich bind
unterscheidet sich noch vom Typ von (+)
.
bind
erwartet im zweiten Parameter eine Funktion. Dies ist dafür da,
um einen schon monadischen Wert mit etwas zu verknüpfen, dass beim
"verbinden" ebenfalls monadisch wird.
Wenn man jetzt den Typen folgt, so sind Ausdrücke der Form:
1 + (round 3.1415)
gültig. Das ist jetzt noch nicht exakt so, wie man
die Addition erwarten würde.
Was wir bisher unterschlagen haben ist, dass es noch eine weitere bind
Funktion gibt. In Haskell, diese beiden:
{% highlight haskell %} (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b {% endhighlight %}
Dabei entspricht der Operator (>>
)= dem bind
wie wir es weiter oben
verwendet haben. Der zweite Operator wurde deswegen unterschlagen, da er
nur eine speziellere Form des ersten Operators ist.
In unserem Fall sind a
und b
gleich und damit ist jetzt auch klar,
dass (>>)
mit dem Type :: m a -> m a -> m a
genau dem (+)
Operator
mit dem Type :: Int -> Int -> Int
entspricht.
Unsere Monade erfüllt obendrein auch noch die Monaden-Gesetzte. Denn als
Identität können wir die 0
angeben. Die 0
ist sowohl linke, als auch
rechte Identität, denn egal wo man 0
addiert, diese Operation
verändert das Ergebnis nicht. Auch die Assoziativität ist erfüllt, denn
die Addition ist von Haus aus Assoziativ (sprich, es ist egal ob ich (1
- 2) + 3 oder 1 + (2 + 3) rechne).
Und was bringt mir das?
Kurz gesagt, nichts, oder alles. Das obige Beispiel hatte nur den Zweck,
zu verdeutlichen, dass eine Monade erstmal nichts kompliziertes ist.
Eine Monade hat man immer dann, wenn man zu einem Datentyp die
Funktionen bind
und return
angeben kann; sowie wenn alle zusammen
die Monaden-Gesetzte erfüllen.
Was das Beispiel auch verdeutlicht, ist die One-Way Eigenschaft von Monaden. Eine einmal gerundete Zahl lässt sich nicht mehr in die ursprüngliche reelle Zahl zurück überführen.
Was könnte man denn mit einer Monade anstellen?
Naja, die Möglichkeiten sind vielfältig. Bezogen auf das obige Beispiel könnte man die reelle Zahl in zwei Integer überführen, wobei einer den ganzzahligen Anteil darstellt, und der andere den Wert nach dem Komma.
{% highlight haskell %} type TwoParts = (Int, Int) {% endhighlight %}
Die return
-Funktion würde also aus einer reellen Zahl ein Tupel
erzeugen. Die bind
-Funktion welche (wie im obigen Beispiel) zwei
dieser Werte additiv miteinander verknüpft, müsste nun die beiden ersten
Teile des Tupels einfach addieren. Die beiden hinteren Teile würden auch
addiert werden, aber auf eine andere Weise wie die vorderen Teile.
Der Trick einer Monade liegt immer in den beiden Funktionen bind
und
return
.
Und was sind so Typische Monaden?
Es gibt Monaden in allen erdenklichen "Geschmacksrichtungen". Ganz häufig wird beispielsweise die IO-Monade verwendet. Diese Monade führt in ihrem monadischen Datenyp einen Wert mit, der z.B. den Zustand des Bildschirmes enthält. Aber auch der Zustand der Tastatur (also welche Tasten wurden gedrückt) stecken in der IO-Monade.
Der Datentyp Maybe a
ist eine Monade. Hier sind die Funktionen bind
und return
sogar recht simpel. Als return
reicht es aus, dem
übergebenen Wert einfach ein Just
voran zu stellen. Die Operation
return 1
liefert Just 1
als Ergebnis, wenn wir von der
Maybe
-Monade reden.
Die bind
-Funktion (und hier ergibt nun tatsächlich auch der
(>>=)-Operator Sinn) bekommt im ersten Parameter eine Monade übergeben.
Im zweiten Parameter muss eine Funktion angegeben werden. Diese Funktion
nimmt einen Wert entgegen und kann den dann für weitere Berechnungen
verwenden. Beispielsweise könnte man den Wert um eins erhöhen.
{% highlight haskell %} return 1 >>= \x -> return (x + 1) > Just 2 {% endhighlight %}
Aber auch der folgende Fall ist gültig, da auch Nothing
ein gültiger
Wert der Maybe
-Monade ist.
{% highlight haskell %} Nothing >>= \x -> return (x + 1) > Nothing {% endhighlight %}
In der Maybe
-Monade lassen sich also Berechnungen angeben, die dann
ausgeführt werden, wenn tatsächlich ein Wert übergeben wurde. Im Falle,
dass kein Wert übergeben wurde, liefert die Berechnung auch nur
Nothing
zurück. Also genau die Funktionalität die man von einer
Maybe
-Monade auch spontan erwarten würde.
Ausblick
Zum Schluss kann ich noch folgende Links zum Thema Monaden empfehlen:
- Monaden in der Wikipedia
- A tour to Haskells Monads
- Monads in JavaScript
- Zeitline zu Monaden-Tutorials
Und dann mag ich nicht enden ohne darauf hinzuweisen, dass das Gerücht kursiert, dass man Monaden erst nach dem 7. Tutorial verstehen kann. Witzigerweise ging mir selber das genau so. Ich kann also jedem nur Mut machen, der Monaden nicht sofort nachvollziehen kann. Lasst etwas Zeit verstreichen und lest dann einfach noch ein weiteres Tutorial zu Monaden. Irgendwann macht's klick, ich bin mir sicher ;-)