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:

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 ;-)