Während ich Haskell lernte, habe ich viele Tutorials durchlaufen, um zu erklären, was Monaden sind und warum Monaden in Haskell wichtig sind. Jeder von ihnen verwendete Analogien, um die Bedeutung besser erfassen zu können. Am Ende des Tages habe ich 3 verschiedene Ansichten darüber, was eine Monade ist:
Ansicht 1: Monade als Etikett
Manchmal denke ich, dass eine Monade als Etikett für einen bestimmten Typ. Zum Beispiel eine Funktion vom Typ:
myfunction :: IO Int
myfunction ist eine Funktion, die bei jeder Ausführung einen Int-Wert liefert. Die Art des Ergebnisses ist nicht Int, sondern IO Int. IO ist also eine Bezeichnung für den Int-Wert, die den Benutzer darauf hinweist, dass der Int-Wert das Ergebnis eines Prozesses ist, bei dem eine IO-Aktion ausgeführt wurde.
Folglich wurde dieser Int-Wert als Wert markiert, der von einem Prozess mit E / A stammt, daher ist dieser Wert "schmutzig". Ihr Prozess ist nicht mehr rein.
Ansicht 2: Monade als privater Raum, in dem schlimme Dinge passieren können.
In einem System, in dem alle Prozesse rein und streng sind, müssen manchmal Nebenwirkungen auftreten. Eine Monade ist also nur ein kleiner Bereich, in dem Sie böse Nebenwirkungen auslösen können. In diesem Raum können Sie der reinen Welt entfliehen, unrein werden, Ihren Prozess machen und dann mit einem Wert zurückkehren.
Ansicht 3: Monade wie in der Kategorietheorie
Diese Ansicht verstehe ich nicht ganz. Eine Monade ist nur ein Funktor derselben Kategorie oder Unterkategorie. Sie haben beispielsweise die Int-Werte und als Unterkategorie IO Int die Int-Werte, die nach einem IO-Prozess generiert wurden.
Sind diese Ansichten korrekt? Welches ist genauer?
Antworten:
Die Ansichten 1 und 2 sind im Allgemeinen falsch.
* -> *
kann als Label fungieren, Monaden sind viel mehr.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 habenx :: 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 habenf :: a -> b
undg :: b -> c
wir bekommeng . f :: a -> c
. Beachten Sie, dass dies auch für unsere konvertierten Werte funktioniert: Wenn wir sie habenx :: a
und in unsere Darstellung konvertieren, erhalten wirf . ((\_ -> x) :: () -> a) :: () -> b
.Diese Darstellung hat einige sehr wichtige Eigenschaften, nämlich:
id :: a -> a
für jeden Typa
. Es ist ein Identitätselement in Bezug auf.
:f
ist gleichf . id
und gleichid . f
..
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 Typfunktionm :: * -> *
. (In Haskell,m
ist[]
,IO
,Maybe
, etc.) und die Kategorie Wille enthält alle Funktionen von Typena -> 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:
<=<
) haben, der Funktionenf :: a -> m b
undg :: b -> m c
in etwas wie zusammensetztg <=< f :: a -> m c
. Und es muss assoziativ sein.return
. Wir wollen auch, dassf <=< return
das dasselbe ist wief
und dasselbe wiereturn <=< f
.Jeder,
m :: * -> *
für den wir solche Funktionen habenreturn
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 transformiertm
.(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 Typenm a
und wir setzen Funktionen von Typen zusammena -> 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 mitreturn
und>>=
sie werden die Monadengesetze genannt .Ein Beispiel - Listen
Wenn wir wählen
m
zu sein[]
, haben wir eine Kategorie von Funktionen von Arten erhaltena -> [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 vonf :: a -> [b]
undg :: 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ücktoder mit
>>=
Beachten Sie, dass in diesem Beispiel die Rückgabetypen
[a]
so waren, dass sie möglicherweise keinen Wert vom Typ enthieltena
. In der Tat gibt es keine solche Anforderung für eine Monade, dass der Rückgabetyp solche Werte haben sollte. Einige Monaden haben immer (wieIO
oderState
), andere nicht, wie[]
oderMaybe
.Die IO-Monade
Wie ich bereits erwähnte, ist die
IO
Monade etwas Besonderes. Ein Wert vom TypIO 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 nichtIO a
mit einer reinen Konstruktion beschreiben. HierIO
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:f :: a -> IO b
undg :: 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 dasIO
"Tag" zum Wert hinzu (wir "berechnen" das Ergebnis einfach, indem wir die Umgebung intakt halten).Einige Notizen:
m a
, gibt es keine Möglichkeit, sich derIO
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.IO
Monade programmieren . Aus diesem GrundIO
wird es oft als Sünde des Programmierers bezeichnet .getChar
müssen Funktionen wie einen Ergebnistyp von habenIO something
.quelle
IO
es aus sprachlicher Sicht keine spezielle Semantik gibt. Es ist nichts Besonderes, es verhält sich wie jeder andere Code. Nur die Implementierung der Laufzeitbibliothek ist etwas Besonderes. Es gibt auch einen speziellen Weg, um zu entkommen (unsafePerformIO
). Ich denke, das ist wichtig, weil die Leute oft anIO
ein spezielles Sprachelement oder einen deklarativen Tag denken . Es ist nicht.coerce :: a -> b
, die zwei beliebige Typen konvertiert (und in den meisten Fällen Ihr Programm zum Absturz bringt). Sehen Sie sich dieses Beispiel an - Sie können sogar eine Funktion inInt
usw. umwandeln .runST :: (forall s. GHC.ST.ST s a) -> a
Ansicht 1: Monade als Etikett
"Folglich wurde dieser Int-Wert als Wert markiert, der von einem Prozess mit E / A stammt, daher ist dieser Wert" schmutzig "."
"IO Int" ist im Allgemeinen kein Int-Wert (obwohl es in einigen Fällen wie "return 3" sein kann). Es ist eine Prozedur, die einen Int-Wert ausgibt. Unterschiedliche Ausführungen dieser "Prozedur" können unterschiedliche Int-Werte ergeben.
Eine Monade m ist eine eingebettete (zwingende) "Programmiersprache": In dieser Sprache können einige "Prozeduren" definiert werden. Ein monadischer Wert (vom Typ ma) ist eine Prozedur in dieser "Programmiersprache", die einen Wert vom Typ a ausgibt.
Beispielsweise:
ist eine Prozedur, die einen Wert vom Typ Int ausgibt.
Dann:
ist eine Prozedur, die zwei (möglicherweise unterschiedliche) Ints ausgibt.
Jede solche "Sprache" unterstützt einige Operationen:
Zwei Prozeduren (ma und mb) können "verkettet" werden: Sie können eine größere Prozedur (ma >> mb) erstellen, die aus der ersten und der zweiten besteht.
außerdem kann der Ausgang (a) des ersten den zweiten beeinflussen (ma >> = \ a -> ...);
Eine Prozedur (return x) kann einen konstanten Wert (x) liefern.
Die verschiedenen eingebetteten Programmiersprachen unterscheiden sich in der Art der Dinge, die sie unterstützen, wie zum Beispiel:
quelle
Verwechseln Sie einen monadischen Typ nicht mit der Monadenklasse.
Ein monadischer Typ (dh ein Typ, der eine Instanz der Monadenklasse ist) würde ein bestimmtes Problem lösen (im Prinzip löst jeder monadische Typ ein anderes): State, Random, Maybe, IO. Alle von ihnen sind Typen mit Kontext (was Sie "Label" nennen, aber das macht sie nicht zu einer Monade).
Für alle von ihnen besteht die Notwendigkeit, Operationen nach Wahl zu verketten (eine Operation hängt vom Ergebnis der vorherigen ab). Hier kommt die Monadenklasse ins Spiel: Lassen Sie Ihren Typ (der ein gegebenes Problem löst) eine Instanz der Monadenklasse sein und das Verkettungsproblem ist gelöst.
Siehe Was löst die Monadenklasse?
quelle