Verschiedene Arten, eine Monade zu sehen

29

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?

Oni
quelle
5
# 2 ist nicht das, was eine Monade im Allgemeinen ist. Tatsächlich ist es so ziemlich auf E / A beschränkt und keine nützliche Ansicht (vgl. Was eine Monade nicht ist ). Als "streng" wird im Allgemeinen auch eine Eigenschaft bezeichnet, die Haskell nicht besitzt (nämlich eine strenge Bewertung). Übrigens ändern Monaden das auch nicht (siehe auch Was eine Monade nicht ist).
3
Technisch gesehen ist nur der dritte richtig. Monad ist Endofunctor, für den eine spezielle Operation definiert ist - Promotion und Bindung. Monaden sind zahlreich - eine Listenmonade ist ein perfektes Beispiel dafür, wie man hinter Monaden eine Eingebung findet. Reads-Einrichtungen sind noch besser. Überraschenderweise können Monaden als Werkzeuge verwendet werden, um implizit den Status in einer reinen funktionalen Sprache zu bestimmen. Dies ist keine definierende Eigenschaft von Monaden: Es ist Zufall, dass State Threading in ihren Begriffen implementiert werden kann. Gleiches gilt für IO.
Permeakra
Common Lisp hat einen eigenen Compiler als Teil der Sprache. Haskell hat Monaden.
Will Ness

Antworten:

33

Die Ansichten 1 und 2 sind im Allgemeinen falsch.

  1. Jeder Datentyp * -> *kann als Label fungieren, Monaden sind viel mehr.
  2. (Mit Ausnahme der IOMonade) 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 IOMonade 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 -> bund g :: b -> cwir bekommen g . f :: a -> c. Beachten Sie, dass dies auch für unsere konvertierten Werte funktioniert: Wenn wir sie haben x :: aund 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 -> afür jeden Typ a. Es ist ein Identitätselement in Bezug auf .: fist gleich f . idund 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 mArt * -> *(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, mist [], 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 bund g :: b -> m cin 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 <=< returndas dasselbe ist wie fund dasselbe wie return <=< f.

Jeder, m :: * -> *für den wir solche Funktionen haben returnund 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 aund wir setzen Funktionen von Typen zusammen a -> m b.

Bei jeder von uns erstellten Monade dürfen wir nicht vergessen, dies zu überprüfen returnund <=<haben die Eigenschaften, die wir benötigen: Assoziativität und links / rechts Identität. Ausgedrückt mit returnund >>=sie werden die Monadengesetze genannt .

Ein Beispiel - Listen

Wenn wir wählen mzu 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], gauf 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 IOoder State), andere nicht, wie []oder Maybe.

Die IO-Monade

Wie ich bereits erwähnte, ist die IOMonade etwas Besonderes. Ein Wert vom Typ IO abedeutet einen Wert vom Typ, ader durch Interaktion mit der Programmumgebung erstellt wurde. Daher können wir (im Gegensatz zu allen anderen Monaden) einen Wert vom Typ nicht IO amit einer reinen Konstruktion beschreiben. Hier IOhandelt 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 IOMonade:

  • Zusammensetzung f :: a -> IO bund g :: b -> IO cMittel: Compute , fdass in Wechselwirkung mit der Umgebung, und dann berechnen , gdie verwendet den Wert und berechnet das Ergebnis mit der Umgebung interagiert.
  • returnFü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:

  1. Da monadische Berechnungen immer den Ergebnistyp haben m a, gibt es keine Möglichkeit, sich der IOMonade zu "entziehen" . Die Bedeutung ist: Sobald eine Berechnung mit der Umgebung interagiert, können Sie daraus keine Berechnung mehr erstellen, die dies nicht tut.
  2. 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 IOMonade programmieren . Aus diesem Grund IOwird es oft als Sünde des Programmierers bezeichnet .
  3. 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 getCharmüssen Funktionen wie einen Ergebnistyp von haben IO something.
Petr Pudlák
quelle
3
Gute Antwort. Ich würde klarstellen, dass IOes 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 an IOein spezielles Sprachelement oder einen deklarativen Tag denken . Es ist nicht.
usr
2
@ usr Guter Punkt. Ich würde hinzufügen, dass unsafePerformIO wirklich unsicher ist und nur von Experten verwendet werden sollte. Damit können Sie alles auflösen. Sie können beispielsweise eine Funktion erstellen 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 in Intusw. umwandeln .
Petr Pudlák
Eine andere "spezielle magische" Monade wäre ST, mit der Sie Referenzen auf Speicher deklarieren können, aus denen Sie nach Belieben lesen und in die Sie schreiben können (allerdings nur innerhalb der Monade). Anschließend können Sie ein Ergebnis extrahieren, indem SierunST :: (forall s. GHC.ST.ST s a) -> a
sara
5

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:

foo :: IO Int

ist eine Prozedur, die einen Wert vom Typ Int ausgibt.

Dann:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

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:

  • Zufällige Werte ergeben;
  • "Gabeln" (die [] Monade);
  • Ausnahmen (werfen / fangen) (The Either e Monad);
  • explizite Fortsetzung / Callcc-Unterstützung;
  • Senden / Empfangen von Nachrichten an andere "Agenten";
  • Erstellen, Setzen und Lesen von Variablen (lokal für diese Programmiersprache) (die ST-Monade).
ysdx
quelle
1

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?

cibercitizen1
quelle