Die Ansichten 1 und 2 sind im Allgemeinen falsch.
- Jeder Datentyp
* -> *
kann als Label fungieren, Monaden sind viel mehr.
- (Mit Ausnahme der
IO
Monade) Berechnungen innerhalb einer Monade sind nicht unrein. Sie stellen einfach Berechnungen dar, die wir als Nebenwirkungen wahrnehmen, aber sie sind rein.
Beide Missverständnisse sind darauf zurückzuführen, dass man sich auf die IO
Monade konzentriert, was eigentlich etwas Besonderes ist.
Ich werde versuchen, auf # 3 etwas näher einzugehen, ohne auf die Kategorietheorie einzugehen, wenn dies möglich ist.
Standardberechnungen
Alle Berechnungen in einer funktionalen Programmiersprache mit einem Quelltyp und einem Zieltyp als Funktionen betrachtet werden: f :: a -> b
. Wenn eine Funktion mehr als ein Argument hat, können wir es durch Currying in eine Funktion mit einem Argument umwandeln (siehe auch Haskell-Wiki ). Und wenn wir nur einen Wert haben x :: a
(eine Funktion mit 0 Argumenten), können wir es in eine Funktion umwandeln , die ein Argument des nimmt Gerätetypen : (\_ -> x) :: () -> a
.
Wir können komplexere Programme aus einfacheren zusammensetzen, indem wir solche Funktionen mit dem .
Operator erstellen . Zum Beispiel, wenn wir haben f :: a -> b
und g :: b -> c
wir bekommen g . f :: a -> c
. Beachten Sie, dass dies auch für unsere konvertierten Werte funktioniert: Wenn wir sie haben x :: a
und in unsere Darstellung konvertieren, erhalten wir f . ((\_ -> x) :: () -> a) :: () -> b
.
Diese Darstellung hat einige sehr wichtige Eigenschaften, nämlich:
- Wir haben eine ganz besondere Funktion - die Identität Funktion
id :: a -> a
für jeden Typ a
. Es ist ein Identitätselement in Bezug auf .
: f
ist gleich f . id
und gleich id . f
.
- Der Funktionszusammensetzungsoperator
.
ist assoziativ .
Monadische Berechnungen
Angenommen, wir möchten eine bestimmte Kategorie von Berechnungen auswählen und damit arbeiten, deren Ergebnis mehr als nur den einzelnen Rückgabewert enthält. Wir wollen nicht spezifizieren, was "etwas mehr" bedeutet, wir wollen die Dinge so allgemein wie möglich halten. Die allgemeinste Art, "etwas mehr" darzustellen, besteht darin, es als eine Typfunktion darzustellen - eine m
Art * -> *
(dh, es konvertiert einen Typ in einen anderen). Für jede Kategorie von Berechnungen, mit denen wir arbeiten möchten, haben wir eine Typfunktion m :: * -> *
. (In Haskell, m
ist []
, IO
, Maybe
, etc.) und die Kategorie Wille enthält alle Funktionen von Typen a -> m b
.
Nun möchten wir mit den Funktionen in einer solchen Kategorie genauso arbeiten wie im Grundfall. Wir wollen diese Funktionen komponieren können, wir wollen, dass die Komposition assoziativ ist und wir wollen eine Identität haben. Wir brauchen:
- Einen Operator (nennen wir ihn
<=<
) haben, der Funktionen f :: a -> m b
und g :: b -> m c
in etwas wie zusammensetzt g <=< f :: a -> m c
. Und es muss assoziativ sein.
- Nennen wir es, um für jeden Typ eine Identitätsfunktion zu haben
return
. Wir wollen auch, dass f <=< return
das dasselbe ist wie f
und dasselbe wie return <=< f
.
Jeder, m :: * -> *
für den wir solche Funktionen haben return
und der <=<
als Monade bezeichnet wird . Es erlaubt uns, komplexe Berechnungen aus einfacheren zu erstellen, genau wie im Grundfall, aber jetzt werden die Arten von Rückgabewerten von transformiert m
.
(Eigentlich habe ich den Begriff Kategorie hier leicht missbraucht . Im Sinne der Kategorietheorie können wir unsere Konstruktion erst dann als Kategorie bezeichnen, wenn wir wissen, dass sie diesen Gesetzen entspricht.)
Monaden in Haskell
In Haskell (und anderen funktionalen Sprachen) arbeiten wir hauptsächlich mit Werten, nicht mit Funktionen von Typen () -> a
. Anstatt <=<
für jede Monade zu definieren, definieren wir eine Funktion (>>=) :: m a -> (a -> m b) -> m b
. Eine solche alternative Definition entspricht, können wir ausdrücken >>=
Verwendung <=<
kehrt und umge (versuchen als eine Übung, oder siehe die Quellen ). Das Prinzip ist jetzt weniger offensichtlich, aber es bleibt dasselbe: Unsere Ergebnisse sind immer von Typen m a
und wir setzen Funktionen von Typen zusammen a -> m b
.
Bei jeder von uns erstellten Monade dürfen wir nicht vergessen, dies zu überprüfen return
und <=<
haben die Eigenschaften, die wir benötigen: Assoziativität und links / rechts Identität. Ausgedrückt mit return
und >>=
sie werden die Monadengesetze genannt .
Ein Beispiel - Listen
Wenn wir wählen m
zu sein []
, haben wir eine Kategorie von Funktionen von Arten erhalten a -> [b]
. Solche Funktionen stellen nicht deterministische Berechnungen dar, deren Ergebnisse ein oder mehrere Werte, aber auch keine Werte sein können. Daraus entsteht die sogenannte Listenmonade . Die Zusammensetzung von f :: a -> [b]
und g :: b -> [c]
funktioniert wie folgt: g <=< f :: a -> [c]
bedeutet, alle möglichen Ergebnisse eines Typs zu berechnen [b]
, g
auf jedes von ihnen anzuwenden und alle Ergebnisse in einer einzigen Liste zusammenzufassen. In Haskell ausgedrückt
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
oder mit >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Beachten Sie, dass in diesem Beispiel die Rückgabetypen [a]
so waren, dass sie möglicherweise keinen Wert vom Typ enthielten a
. In der Tat gibt es keine solche Anforderung für eine Monade, dass der Rückgabetyp solche Werte haben sollte. Einige Monaden haben immer (wie IO
oder State
), andere nicht, wie []
oder Maybe
.
Die IO-Monade
Wie ich bereits erwähnte, ist die IO
Monade etwas Besonderes. Ein Wert vom Typ IO a
bedeutet einen Wert vom Typ, a
der durch Interaktion mit der Programmumgebung erstellt wurde. Daher können wir (im Gegensatz zu allen anderen Monaden) einen Wert vom Typ nicht IO a
mit einer reinen Konstruktion beschreiben. Hier IO
handelt es sich einfach um ein Tag oder eine Bezeichnung, die Berechnungen unterscheidet, die mit der Umgebung interagieren. Dies ist (der einzige Fall), in dem die Ansichten Nr. 1 und Nr. 2 korrekt sind.
Für die IO
Monade:
- Zusammensetzung
f :: a -> IO b
und g :: b -> IO c
Mittel: Compute , f
dass in Wechselwirkung mit der Umgebung, und dann berechnen , g
die verwendet den Wert und berechnet das Ergebnis mit der Umgebung interagiert.
return
Fügt nur das IO
"Tag" zum Wert hinzu (wir "berechnen" das Ergebnis einfach, indem wir die Umgebung intakt halten).
- Die Monadengesetze (Assoziativität, Identität) werden vom Compiler garantiert.
Einige Notizen:
- Da monadische Berechnungen immer den Ergebnistyp haben
m a
, gibt es keine Möglichkeit, sich der IO
Monade zu "entziehen" . Die Bedeutung ist: Sobald eine Berechnung mit der Umgebung interagiert, können Sie daraus keine Berechnung mehr erstellen, die dies nicht tut.
- Wenn ein funktionaler Programmierer nicht weiß, wie man etwas rein macht, kann er (als letzte Möglichkeit) die Aufgabe durch eine zustandsbezogene Berechnung innerhalb der
IO
Monade programmieren . Aus diesem Grund IO
wird es oft als Sünde des Programmierers bezeichnet .
- Beachten Sie, dass in einer unreinen Welt (im Sinne der funktionalen Programmierung) das Lesen eines Werts auch die Umgebung verändern kann (z. B. Eingaben des Benutzers verbrauchen). Deshalb
getChar
müssen Funktionen wie einen Ergebnistyp von haben IO something
.