Warum werden Nebenwirkungen in Haskell als Monaden modelliert?

172

Könnte jemand einige Hinweise geben, warum die unreinen Berechnungen in Haskell als Monaden modelliert werden?

Ich meine, Monade ist nur eine Schnittstelle mit 4 Operationen. Was war also der Grund für die Modellierung von Nebenwirkungen?

bodacydo
quelle
15
Monaden definieren nur zwei Operationen.
Dario
3
aber was ist mit Rückkehr und Scheitern? (neben (>>) und (>> =))
bodacydo
55
Die beiden Operationen sind returnund (>>=). x >> yist dasselbe wie x >>= \\_ -> y(dh es ignoriert das Ergebnis des ersten Arguments). Wir reden nicht darüber fail.
porges
2
@Porges Warum nicht über Fail sprechen? Es ist etwas nützlich in zB Vielleicht, Parser usw.
Alternative
16
@monadic: failist Monadwegen eines historischen Unfalls in der Klasse; es gehört wirklich in MonadPlus. Beachten Sie, dass die Standarddefinition unsicher ist.
JB.

Antworten:

292

Angenommen, eine Funktion hat Nebenwirkungen. Wenn wir alle Effekte, die es erzeugt, als Eingabe- und Ausgabeparameter verwenden, ist die Funktion für die Außenwelt rein.

Also für eine unreine Funktion

f' :: Int -> Int

Wir fügen der Betrachtung die RealWorld hinzu

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

dann fist wieder rein. Wir definieren einen parametrisierten Datentyp type IO a = RealWorld -> (a, RealWorld), sodass wir RealWorld nicht so oft eingeben müssen und nur schreiben können

f :: Int -> IO Int

Für den Programmierer ist die direkte Handhabung einer RealWorld zu gefährlich. Insbesondere wenn ein Programmierer einen Wert vom Typ RealWorld in die Hände bekommt, versucht er möglicherweise, ihn zu kopieren , was im Grunde unmöglich ist. (Denken Sie beispielsweise daran, das gesamte Dateisystem zu kopieren. Wo würden Sie es ablegen?) Daher umfasst unsere Definition von E / A auch die Zustände der ganzen Welt.

Zusammensetzung "unreiner" Funktionen

Diese unreinen Funktionen sind nutzlos, wenn wir sie nicht miteinander verketten können. Erwägen

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

Wir wollen

  • Holen Sie sich einen Dateinamen von der Konsole,
  • Lesen Sie diese Datei und
  • Drucken Sie den Inhalt dieser Datei auf die Konsole.

Wie würden wir es tun, wenn wir Zugang zu den Staaten der realen Welt hätten?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Wir sehen hier ein Muster. Die Funktionen heißen folgendermaßen:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Wir könnten also einen Operator definieren ~~~, um sie zu binden:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

dann könnten wir einfach schreiben

printFile = getLine ~~~ getContents ~~~ putStrLn

ohne die reale Welt zu berühren.

"Verunreinigung"

Angenommen, wir möchten den Dateiinhalt auch in Großbuchstaben schreiben. Großbuchstaben sind eine reine Funktion

upperCase :: String -> String

Aber um es in die reale Welt zu schaffen, muss es eine zurückgeben IO String. Es ist einfach, eine solche Funktion aufzuheben:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Dies kann verallgemeinert werden:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

so dass impureUpperCase = impurify . upperCase, und wir können schreiben

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Hinweis: Normalerweise schreiben wir getLine ~~~ getContents ~~~ (putStrLn . upperCase))

Wir haben die ganze Zeit mit Monaden gearbeitet

Nun wollen wir sehen, was wir getan haben:

  1. Wir haben einen Operator definiert, (~~~) :: IO b -> (b -> IO c) -> IO cder zwei unreine Funktionen miteinander verkettet
  2. Wir haben eine Funktion definiert impurify :: a -> IO a, die einen reinen Wert in unrein umwandelt.

Jetzt machen wir die Identifizierung (>>=) = (~~~)und return = impurify, und sehen? Wir haben eine Monade.


Technischer Hinweis

Um sicherzustellen, dass es sich wirklich um eine Monade handelt, müssen noch einige Axiome überprüft werden:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
    
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
    
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    Links als Übung.

kennytm
quelle
5
+1, aber ich möchte darauf hinweisen, dass dies speziell den E / A-Fall abdeckt. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html ist ziemlich ähnlich, verallgemeinert sich aber RealWorldin ... na ja , Sie werden sehen.
Ephemient
4
Beachten Sie, dass diese Erklärung nicht wirklich für Haskell gelten kann IO, da letztere Interaktion, Parallelität und Nichtdeterminismus unterstützt. Weitere Hinweise finden Sie in meiner Antwort auf diese Frage.
Conal
2
@Conal GHC implementiert tatsächlich auf IOdiese Weise, RealWorldrepräsentiert jedoch nicht die reale Welt. Es ist nur ein Zeichen, um die Operationen in Ordnung zu halten (die "Magie" ist, dass RealWorldGHC Haskells einziger Einzigartigkeitstyp ist)
Jeremy List
2
@JeremyList Soweit ich weiß, implementiert GHC IOeine Kombination aus dieser Darstellung und nicht standardmäßiger Compilermagie (die an den berühmten C-Compiler-Virus von Ken Thompson erinnert ). Bei anderen Typen liegt die Wahrheit im Quellcode zusammen mit der üblichen Haskell-Semantik.
Conal
1
@Clonal Mein Kommentar war darauf zurückzuführen, dass ich die relevanten Teile des GHC-Quellcodes gelesen hatte.
Jeremy List
43

Könnte jemand einige Hinweise geben, warum die unreinen Berechnungen in Haskell als Monaden modelliert werden?

Diese Frage enthält ein weit verbreitetes Missverständnis. Unreinheit und Monade sind unabhängige Begriffe. Verunreinigungen werden von Monad nicht modelliert. Vielmehr gibt es einige Datentypen, die beispielsweise eine IOzwingende Berechnung darstellen. Und für einige dieser Typen entspricht ein winziger Bruchteil ihrer Schnittstelle dem Schnittstellenmuster "Monad". Darüber hinaus ist keine reine / funktionale / denotative Erklärung bekannt IO(und es ist unwahrscheinlich, dass es eine gibt, wenn man den Zweck von "sin bin" berücksichtigt IO), obwohl es die allgemein erzählte Geschichte über World -> (a, World)die Bedeutung von gibt IO a. Diese Geschichte kann nicht wahrheitsgemäß beschreiben IO, weilIOunterstützt Parallelität und Nichtdeterminismus. Die Geschichte funktioniert nicht einmal bei deterministischen Berechnungen, die eine Interaktion mit der Welt während der Berechnung ermöglichen.

Weitere Erklärungen finden Sie in dieser Antwort .

Bearbeiten : Beim erneuten Lesen der Frage denke ich nicht, dass meine Antwort ganz auf dem richtigen Weg ist. Modelle der imperativen Berechnung erweisen sich oft als Monaden, genau wie die Frage sagte. Der Fragesteller geht möglicherweise nicht wirklich davon aus, dass Monadität in irgendeiner Weise die Modellierung imperativer Berechnungen ermöglicht.

Conal
quelle
1
@KennyTM: Aber RealWorldist, wie die Dokumente sagen, "zutiefst magisch". Es ist ein Token, das darstellt, was das Laufzeitsystem tut. Es bedeutet eigentlich nichts über die reale Welt. Sie können nicht einmal einen neuen heraufbeschwören, um einen "Thread" zu erstellen, ohne zusätzliche Tricks zu machen. Der naive Ansatz würde nur eine einzige blockierende Aktion mit viel Unklarheit darüber erzeugen, wann sie ausgeführt wird.
CA McCann
4
Ich würde auch argumentieren, dass Monaden von Natur aus unerlässlich sind . Wenn der Funktor eine Struktur mit darin eingebetteten Werten darstellt, bedeutet eine Monadeninstanz, dass Sie basierend auf diesen Werten neue Ebenen erstellen und reduzieren können. Unabhängig von der Bedeutung, die Sie einer einzelnen Ebene des Funktors zuweisen, bedeutet eine Monade, dass Sie eine unbegrenzte Anzahl von Ebenen mit einem strengen Begriff der Kausalität erstellen können, der von einer zur nächsten geht. Bestimmte Instanzen haben möglicherweise keine intrinsisch zwingende Struktur, aber Monadim Allgemeinen wirklich.
CA McCann
3
Mit " Monadim Allgemeinen" meine ich grob forall m. Monad m => ..., dh an einer beliebigen Instanz arbeiten. Die Dinge, die Sie mit einer beliebigen Monade tun können, sind fast genau die gleichen Dinge, mit denen Sie tun können IO: undurchsichtige Grundelemente (als Funktionsargumente bzw. aus Bibliotheken) empfangen, No-Ops mit erstellen returnoder einen Wert auf irreversible Weise mit transformieren (>>=). Die Essenz der Programmierung in einer beliebigen Monade besteht darin, eine Liste unwiderruflicher Aktionen zu erstellen: "mache X, dann mache Y, dann ...". Klingt für mich ziemlich zwingend!
CA McCann
2
Nein, du vermisst meinen Standpunkt hier immer noch. Natürlich würden Sie diese Denkweise für keinen dieser spezifischen Typen verwenden, da sie eine klare, aussagekräftige Struktur haben. Wenn ich "willkürliche Monaden" sage, meine ich "Sie können nicht auswählen, welche"; Die Perspektive hier ist aus dem Quantifizierer heraus, daher mkönnte es hilfreicher sein, als existentiell zu denken . Darüber hinaus ist meine "Interpretation" eine Neuformulierung der Gesetze; Die Liste der "do X" -Anweisungen ist genau das freie Monoid auf der unbekannten Struktur, die über erstellt wurde (>>=). und die Monadengesetze sind nur monoide Gesetze zur Endofunktorkomposition.
CA McCann
3
Kurz gesagt, die größte Untergrenze für das, was alle Monaden zusammen beschreiben, ist ein blinder, bedeutungsloser Marsch in die Zukunft. IOist ein pathologischer Fall, gerade weil er fast nichts mehr als dieses Minimum bietet . In bestimmten Fällen können Typen mehr Struktur aufweisen und somit eine tatsächliche Bedeutung haben. Ansonsten stehen die wesentlichen Eigenschaften einer Monade - basierend auf den Gesetzen - der eindeutigen Bezeichnung ebenso entgegen wie sie IOist. Ohne den Export von Konstruktoren, die erschöpfende Aufzählung primitiver Aktionen oder ähnliches ist die Situation hoffnungslos.
CA McCann
13

So wie ich es verstehe, bemerkte jemand namens Eugenio Moggi zuerst, dass ein zuvor obskures mathematisches Konstrukt namens "Monade" verwendet werden könnte, um Nebenwirkungen in Computersprachen zu modellieren und daher ihre Semantik mithilfe der Lambda-Rechnung zu spezifizieren. Als Haskell entwickelt wurde, gab es verschiedene Möglichkeiten, unreine Berechnungen zu modellieren (siehe Simon Peyton Jones ' "Haarhemd" -Papier für weitere Details), aber als Phil Wadler Monaden einführte, wurde schnell klar, dass dies die Antwort war. Und der Rest ist Geschichte.

Paul Johnson
quelle
3
Nicht ganz. Es ist bekannt, dass eine Monade die Interpretation für eine sehr lange Zeit modellieren kann (zumindest seit "Topoi: Eine kategoriale Analyse der Logik"). Andererseits war es nicht möglich, die Typen für Monaden klar auszudrücken, bis sie stark typisiert funktional waren Sprachen kamen herum, und dann setzte Moggi zwei und zwei zusammen.
Nomen
1
Vielleicht könnten Monaden leichter zu verstehen sein, wenn sie als Kartenumbruch und -auspack definiert würden, wobei return ein Synonym für Wrap ist.
aoeu256
9

Könnte jemand einige Hinweise geben, warum die unreinen Berechnungen in Haskell als Monaden modelliert werden?

Nun, weil Haskell rein ist . Sie benötigen ein mathematisches Konzept zu unterscheiden zwischen unreinen Berechnungen und Reinen auf Typ-Ebene und zum Modellprogramm fließt jeweils in.

Dies bedeutet, dass Sie am Ende einen Typ haben müssen IO a, der eine unreine Berechnung modelliert. Dann müssen Sie wissen, wie Sie diese Berechnungen kombinieren können, die in Sequenz ( >>=) gelten und einen Wert ( return) anheben. Dies sind die offensichtlichsten und grundlegendsten.

Mit diesen beiden haben Sie bereits eine Monade definiert (ohne daran zu denken);)

Darüber hinaus bieten Monaden sehr allgemein und mächtige Abstraktionen , können so viele Arten von Steuerfluss bequem in monadischen Funktionen verallgemeinert werden , wie sequence, liftModer spezielle Syntax, unpureness nicht so einen Sonderfall machen.

Weitere Informationen finden Sie unter Monaden in funktionaler Programmierung und Eindeutigkeitstypisierung (die einzige mir bekannte Alternative).

Dario
quelle
6

Wie Sie sagen, Monadist eine sehr einfache Struktur. Die eine Hälfte der Antwort lautet: Monadist die einfachste Struktur, die wir möglicherweise nebenwirkenden Funktionen geben und sie verwenden können. Mit Monadkönnen wir zwei Dinge tun: Wir können einen reinen Wert als Nebenwirkungswert behandeln ( return), und wir können eine Nebenwirkungsfunktion auf einen Nebenwirkungswert anwenden, um einen neuen Nebenwirkungswert zu erhalten ( >>=). Die Fähigkeit zu verlieren, eines dieser Dinge zu tun, wäre lähmend, daher muss unser Nebeneffekttyp "mindestens" sein Monad, und es stellt sich heraus, dass Monades ausreicht, alles zu implementieren, was wir bisher benötigt haben.

Die andere Hälfte ist: Was ist die detaillierteste Struktur, die wir "möglichen Nebenwirkungen" geben könnten? Wir können uns den Raum aller möglichen Nebenwirkungen durchaus als Satz vorstellen (die einzige Operation, die erforderlich ist, ist die Mitgliedschaft). Wir können zwei Nebenwirkungen kombinieren, indem wir sie nacheinander ausführen. Dies führt zu einer anderen Nebenwirkung (oder möglicherweise zur gleichen - wenn die erste "Computer herunterfahren" und die zweite "Datei schreiben" war, dann das Ergebnis diese zu komponieren ist nur "Computer herunterfahren").

Ok, was können wir zu dieser Operation sagen? Es ist assoziativ; Das heißt, wenn wir drei Nebenwirkungen kombinieren, spielt es keine Rolle, in welcher Reihenfolge wir das Kombinieren durchführen. Wenn wir dies tun (Datei schreiben, dann Socket lesen) und dann den Computer herunterfahren, ist es dasselbe wie dann eine Datei schreiben (Socket lesen, dann herunterfahren) Computer). Aber es ist nicht kommutativ: ("Datei schreiben", dann "Datei löschen") ist ein anderer Nebeneffekt als ("Datei löschen", dann "Datei schreiben"). Und wir haben eine Identität: Der spezielle Nebeneffekt "keine Nebenwirkungen" funktioniert ("keine Nebenwirkungen", dann "Datei löschen" ist der gleiche Nebeneffekt wie nur "Datei löschen"). An diesem Punkt denkt jeder Mathematiker "Gruppe!" Aber Gruppen haben Umkehrungen, und es gibt keine Möglichkeit, eine Nebenwirkung im Allgemeinen umzukehren. "Datei löschen" ist irreversibel. Die Struktur, die wir übrig haben, ist die eines Monoids, was bedeutet, dass unsere Nebenwirkungen Monaden sein sollten.

Gibt es eine komplexere Struktur? Sicher! Wir könnten mögliche Nebenwirkungen in dateisystembasierte Effekte, netzwerkbasierte Effekte und mehr unterteilen und ausgefeiltere Kompositionsregeln entwickeln, die diese Details bewahren. Aber es kommt wieder darauf an: Monadist sehr einfach und dennoch leistungsfähig genug, um die meisten Eigenschaften auszudrücken, die uns wichtig sind. (Insbesondere die Assoziativität und die anderen Axiome lassen uns unsere Anwendung in kleinen Stücken testen, mit der Gewissheit, dass die Nebenwirkungen der kombinierten Anwendung mit der Kombination der Nebenwirkungen der Teile übereinstimmen).

lmm
quelle
4

Es ist eigentlich eine ziemlich saubere Art, E / A auf funktionale Weise zu betrachten.

In den meisten Programmiersprachen führen Sie Eingabe- / Ausgabeoperationen aus. In Haskell, das Schreiben von Code vorstellen , nicht zu tun , die Operationen, sondern eine Liste der Operationen zu erzeugen , die Sie tun mögen.

Monaden sind einfach eine hübsche Syntax für genau das.

Wenn Sie wissen möchten, warum Monaden im Gegensatz zu etwas anderem stehen, ist die Antwort wahrscheinlich, dass sie die beste funktionale Art sind, E / A darzustellen, an die die Leute denken konnten, als sie Haskell machten.

Noah Lavine
quelle
3

AFAIK, der Grund ist, Nebenwirkungsprüfungen in das Typensystem aufnehmen zu können. Wenn Sie mehr wissen möchten, hören Sie sich diese SE-Radio- Episoden an: Episode 108: Simon Peyton Jones über funktionale Programmierung und Haskell Episode 72: Erik Meijer über LINQ

Gabriel Ščerbák
quelle
2

Oben finden Sie sehr gute detaillierte Antworten mit theoretischem Hintergrund. Aber ich möchte meine Meinung zur IO-Monade äußern. Ich bin kein erfahrener Haskell-Programmierer, also kann es sein, dass es ziemlich naiv oder sogar falsch ist. Aber ich habe mir geholfen, bis zu einem gewissen Grad mit IO-Monaden umzugehen (beachten Sie, dass sie sich nicht auf andere Monaden beziehen).

Zunächst möchte ich sagen, dass dieses Beispiel mit "realer Welt" für mich nicht allzu klar ist, da wir nicht auf seine (realen) früheren Zustände zugreifen können. Möglicherweise bezieht es sich überhaupt nicht auf Monadenberechnungen, aber es ist im Sinne einer referenziellen Transparenz erwünscht, die im Allgemeinen im Haskell-Code enthalten ist.

Wir wollen also, dass unsere Sprache (haskell) rein ist. Aber wir brauchen Eingabe- / Ausgabeoperationen, da unser Programm ohne sie nicht nützlich sein kann. Und diese Operationen können von Natur aus nicht rein sein. Der einzige Weg, um damit umzugehen, besteht darin, unreine Operationen vom Rest des Codes zu trennen.

Hier kommt die Monade. Eigentlich bin ich mir nicht sicher, ob es kein anderes Konstrukt mit ähnlich benötigten Eigenschaften geben kann, aber der Punkt ist, dass Monaden diese Eigenschaften haben, damit sie verwendet werden können (und erfolgreich verwendet werden). Die Haupteigenschaft ist, dass wir nicht davon entkommen können. Die Monadenschnittstelle hat keine Operationen, um die Monade um unseren Wert herum loszuwerden. Andere (nicht E / A-) Monaden stellen solche Operationen bereit und ermöglichen einen Mustervergleich (z. B. Vielleicht), aber diese Operationen befinden sich nicht in der Monadenschnittstelle. Eine weitere erforderliche Eigenschaft ist die Fähigkeit, Operationen zu verketten.

Wenn wir darüber nachdenken, was wir in Bezug auf das Typsystem benötigen, kommen wir zu der Tatsache, dass wir einen Typ mit Konstruktor benötigen, der um jedes Tal gewickelt werden kann. Der Konstruktor muss privat sein, da wir das Entkommen aus ihm verbieten (dh Mustervergleich). Aber wir brauchen eine Funktion, um diesem Konstruktor einen Wert zu verleihen (hier fällt uns die Rückkehr ein). Und wir brauchen den Weg zur Verkettung. Wenn wir einige Zeit darüber nachdenken, werden wir zu der Tatsache kommen, dass die Verkettungsoperation den Typ >> = haben muss. Wir kommen also zu etwas, das der Monade sehr ähnlich ist. Ich denke, wenn wir jetzt mögliche widersprüchliche Situationen mit diesem Konstrukt analysieren, werden wir zu Monadenaxiomen kommen.

Beachten Sie, dass das entwickelte Konstrukt nichts mit Verunreinigungen gemein hat. Es hat nur Eigenschaften, die wir haben wollten, um mit unreinen Operationen umgehen zu können, nämlich nicht entkommen, verketten und einen Weg, um hineinzukommen.

Nun wird eine Reihe von unreinen Operationen durch die Sprache innerhalb dieser ausgewählten Monaden-E / A vordefiniert. Wir können diese Operationen kombinieren, um neue unreine Operationen zu erstellen. Und all diese Operationen müssen E / A in ihrem Typ haben. Beachten Sie jedoch, dass das Vorhandensein von E / A in der Art einer Funktion diese Funktion nicht unrein macht. Aber so wie ich es verstehe, ist es eine schlechte Idee, reine Funktionen mit IO in ihrem Typ zu schreiben, da es ursprünglich unsere Idee war, reine und unreine Funktionen zu trennen.

Abschließend möchte ich sagen, dass Monaden unreine Operationen nicht in reine verwandeln. Es erlaubt nur, sie effektiv zu trennen. (Ich wiederhole, dass es nur mein Verständnis ist)

Dmitrii Semikin
quelle
1
Sie helfen Ihnen bei der Eingabe von Überprüfungsprogrammen, indem Sie Prüfeffekte eingeben können, und Sie können Ihre eigenen DSLs definieren, indem Sie Monaden erstellen, um die Auswirkungen Ihrer Funktionen einzuschränken, damit der Compiler Ihre Sequenzierungsfehler überprüfen kann.
aoeu256
Dieser Kommentar von aoeu256 ist das "Warum", das in allen bisher gegebenen Erklärungen fehlt. (dh: Monaden sind nicht für Menschen, sondern für Compiler)
João Otero