Ist der Nutzen des IO-Monadenmusters für den Umgang mit Nebenwirkungen rein akademisch?

17

Es tut mir leid für eine weitere Frage zu FP + -Nebenwirkungen, aber ich konnte keine existierende finden, die dies für mich ganz beantwortete.

Mein (begrenztes) Verständnis der funktionalen Programmierung ist, dass Zustände / Nebenwirkungen minimiert und von zustandsloser Logik getrennt werden sollten.

Ich nehme auch Haskells Ansatz dazu zur Kenntnis, die IO-Monade, die dies erreicht, indem sie zustandsbezogene Aktionen zur späteren Ausführung in einen Container packt, der über den Rahmen des Programms hinausgeht.

Ich versuche, dieses Muster zu verstehen, aber tatsächlich zu bestimmen, ob es in einem Python-Projekt verwendet werden soll, also möchte ich Haskell-Besonderheiten vermeiden, wenn möglich.

Rohes Beispiel eingehend.

Wenn mein Programm eine XML-Datei in eine JSON-Datei konvertiert:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

Ist der Ansatz der IO-Monade nicht effektiv, um dies zu tun:

steps = list(
    read_file,
    convert,
    write_file,
)

dann entlasten Sie sich von der Verantwortung, indem Sie diese Schritte nicht tatsächlich aufrufen , sondern den Dolmetscher dies tun lassen?

Oder anders ausgedrückt, es ist wie beim Schreiben:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

Wenn Sie dann erwarten, dass jemand anderes anruft inner()und sagt, dass Ihre Arbeit erledigt ist, weil sie main()rein ist.

Das gesamte Programm wird im Grunde genommen in der IO-Monade enthalten sein.

Wenn der Code tatsächlich ausgeführt wird , hängt alles nach dem Lesen der Datei vom Status der Datei ab, sodass immer noch dieselben statusbezogenen Fehler wie bei der Imperativimplementierung auftreten. Haben Sie also als Programmierer, der dies verwaltet, tatsächlich etwas gewonnen?

Ich weiß den Vorteil des Reduzierens und Isolierens von staatlichem Verhalten sehr zu schätzen , weshalb ich die imperative Version so strukturiert habe: Inputs sammeln, reine Sachen machen, Outputs ausspucken. Hoffentlich convert()kann man völlig rein sein und die Vorteile von Cachefähigkeit, Threadsicherheit usw. nutzen.

Ich weiß auch zu schätzen, dass monadische Typen nützlich sein können, insbesondere in Pipelines, die mit vergleichbaren Typen betrieben werden, aber ich verstehe nicht, warum IO Monaden verwenden sollte, es sei denn, sie befinden sich bereits in einer solchen Pipeline.

Gibt es einen zusätzlichen Vorteil beim Umgang mit Nebenwirkungen, die das E / A-Monadenmuster mit sich bringt, das mir fehlt?

Stu Cox
quelle
1
Sie sollten dieses Video ansehen . Die Wunder der Monaden werden endlich enthüllt, ohne auf Kategorietheorie oder Haskell zurückzugreifen. Es stellt sich heraus, dass Monaden trivial in JavaScript ausgedrückt werden und einer der wichtigsten Faktoren für Ajax sind. Monaden sind unglaublich. Es handelt sich um einfache Dinge, die fast trivial implementiert sind und eine enorme Fähigkeit besitzen, die Komplexität zu verwalten. Aber es ist überraschend schwierig, sie zu verstehen, und die meisten Menschen scheinen, sobald sie diesen ah-ha-Moment haben, die Fähigkeit zu verlieren, sie anderen zu erklären.
Robert Harvey
Gutes Video, danke. Ich habe tatsächlich von einem JS-Intro bis zur funktionalen Programmierung etwas über dieses Zeug gelernt (dann habe ich noch eine Million gelesen…). Obwohl ich das gesehen habe, bin ich mir ziemlich sicher, dass meine Frage spezifisch für die IO-Monade ist, die Crock in diesem Video nicht behandelt.
Stu Cox
Hmm ... Wird AJAX nicht als eine Form von E / A angesehen?
Robert Harvey
1
Beachten Sie, dass der Typ mainin einem Haskell-Programm IO ()- eine E / A-Aktion ist. Dies ist eigentlich gar keine Funktion; Es ist ein Wert . Ihr gesamtes Programm ist ein reiner Wert, der Anweisungen enthält, die der Laufzeitsprache mitteilen, was sie tun soll. Alle unreinen Dinge (die tatsächlich die E / A-Aktionen ausführen) liegen außerhalb des Bereichs Ihres Programms.
Wyzard
In Ihrem Beispiel ist der monadische Teil, wenn Sie das Ergebnis einer Berechnung ( read_file) als Argument für die nächste verwenden ( write_file). Wenn Sie nur eine Sequenz unabhängiger Aktionen hätten, bräuchten Sie keine Monade.
Lortabac

Antworten:

14

Das gesamte Programm wird im Grunde genommen in der IO-Monade enthalten sein.

Das ist der Punkt, an dem ich glaube, dass Sie es aus Sicht der Haskeller nicht sehen. Wir haben also ein Programm wie dieses:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Ich denke, eine typische Einstellung von Haskeller wäre, dass convertder reine Teil:

  1. Ist wahrscheinlich der größte Teil dieses Programms und weitaus komplizierter als die IOTeile;
  2. Kann überlegt und getestet werden, ohne sich IOüberhaupt darum kümmern zu müssen .

Sie sehen dies also nicht als convert"enthalten" an IO, sondern als isoliert von IO. Was immer dies converttut, kann niemals von etwas abhängen, das in einer IOHandlung geschieht .

Wenn der Code tatsächlich ausgeführt wird, hängt alles nach dem Lesen der Datei vom Status der Datei ab, sodass immer noch dieselben statusbezogenen Fehler wie bei der Imperativimplementierung auftreten. Haben Sie also als Programmierer, der dies verwaltet, tatsächlich etwas gewonnen?

Ich würde sagen, dass dies in zwei Dinge unterteilt ist:

  1. Wenn das Programm ausgeführt wird, hängt der Wert des Arguments to convertvom Status der Datei ab.
  2. Aber was die convertFunktion macht , hängt nicht vom Zustand der Datei ab. convertist immer die gleiche Funktion , auch wenn sie an verschiedenen Stellen mit unterschiedlichen Argumenten aufgerufen wird.

Dies ist ein etwas abstrakter Punkt, aber es ist wirklich der Schlüssel zu dem, was Haskellers meinen, wenn sie darüber sprechen. Sie möchten so schreiben convert, dass bei jedem gültigen Argument ein korrektes Ergebnis für dieses Argument erzeugt wird. Wenn Sie es so betrachten, geht die Tatsache, dass das Lesen einer Datei eine zustandsbehaftete Operation ist, nicht in die Gleichung ein. Alles, was zählt, ist, dass jedes Argument, das ihm zugeführt wird und wo immer es herkommt, convertes richtig handhaben muss. Und die Tatsache, dass Reinheit die Möglichkeiten convertder Eingabe einschränkt, vereinfacht diese Überlegungen.

Wenn also convertaus einigen Argumenten falsche Ergebnisse hervorgehen und readFilesie zu einem solchen Argument machen, sehen wir das nicht als Fehler, der durch state verursacht wurde . Es ist ein Fehler in einer reinen Funktion!

Sacundim
quelle
Ich denke, dies ist die beste Beschreibung (obwohl die anderen mir auch geholfen haben, die Dinge zu klären), danke.
Stu Cox
Ist es erwähnenswert, dass die Verwendung von Monaden in Python weniger Vorteile hat, da Python nur einen (statischen) Typ hat und daher keine Garantien für irgendetwas gibt?
jk.
7

Es ist schwer, genau zu wissen, was Sie mit "rein akademisch" meinen, aber ich denke, die Antwort lautet meistens "nein".

Wie in Simon Peyton Jones ' Tackling the Awkward Squad ( dringend empfohlene Lektüre!) Erläutert , sollte monadische E / A echte Probleme mit der Art und Weise lösen, wie Haskell mit E / A umgeht. Lesen Sie das Beispiel des Servers mit Anfragen und Antworten, das ich hier nicht kopieren werde. es ist sehr lehrreich.

Haskell unterstützt im Gegensatz zu Python einen Stil der "reinen" Berechnung, der durch sein Typsystem erzwungen werden kann. Natürlich können Sie sich selbst disziplinieren, wenn Sie in Python programmieren, um diesem Stil zu entsprechen, aber was ist mit Modulen, die Sie nicht geschrieben haben? Ohne viel Hilfe des Typsystems (und der allgemeinen Bibliotheken) ist monadische E / A in Python wahrscheinlich weniger nützlich. Die Philosophie der Sprache ist einfach nicht dazu gedacht, eine strikte Trennung von rein und unrein durchzusetzen.

Beachten Sie, dass dies mehr über die unterschiedlichen Philosophien von Haskell und Python aussagt als darüber, wie akademisch monadisches I / O ist. Ich würde es nicht für Python verwenden.

Eine andere Sache. Du sagst:

Das gesamte Programm wird im Grunde genommen in der IO-Monade enthalten sein.

Es ist wahr, dass die Haskell- mainFunktion "lebt" IO, aber echte Haskell-Programme sollten nicht verwendet werden, IOwenn sie nicht benötigt werden. Fast jede Funktion, die Sie schreiben und für die keine E / A erforderlich ist, sollte keinen Typ haben IO.

Also würde ich in Ihrem letzten Beispiel sagen, dass Sie es falsch verstanden haben: Es mainist unrein (weil es Dateien liest und schreibt), aber Kernfunktionen wie convertsind rein.

Andres F.
quelle
3

Warum ist IO unrein? Weil es zu unterschiedlichen Zeiten unterschiedliche Werte zurückgeben kann. Es gibt eine Zeitabhängigkeit, die auf die eine oder andere Weise berücksichtigt werden muss . Dies ist umso wichtiger, wenn die Auswertung faul ist. Betrachten Sie das folgende Programm:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Warum würde die erste Eingabeaufforderung ohne eine E / A-Monade jemals ausgegeben werden? Es gibt nichts, was davon abhängt, so dass eine faule Bewertung bedeutet, dass es niemals gefordert wird. Es gibt auch nichts, was die Eingabeaufforderung dazu zwingt, ausgegeben zu werden, bevor die Eingabe gelesen wird. Für den Computer sind diese ersten beiden Ausdrücke ohne E / A-Monade völlig unabhängig voneinander. Glücklicherweise nameerlegt er den zweiten beiden einen Befehl auf.

Es gibt andere Möglichkeiten, um das Problem der Auftragsabhängigkeit zu lösen, aber die Verwendung einer E / A-Monade ist wahrscheinlich die einfachste Möglichkeit (zumindest aus sprachlicher Sicht), um zu ermöglichen, dass alles im faulen Funktionsbereich bleibt, ohne kleine Teile des Imperativcodes. Es ist auch das flexibelste. Beispielsweise können Sie relativ einfach eine E / A-Pipeline dynamisch zur Laufzeit basierend auf Benutzereingaben erstellen.

Karl Bielefeldt
quelle
2

Mein (begrenztes) Verständnis der funktionalen Programmierung ist, dass Zustände / Nebenwirkungen minimiert und von zustandsloser Logik getrennt werden sollten.

Das ist nicht nur funktionale Programmierung; Das ist normalerweise eine gute Idee in jeder Sprache. Wenn Sie Unit-Tests durchführen, ist die Art und Weise, wie Sie sich aufteilen read_file(), völlig normal, da das Schreiben von Tests relativ einfach ist , obwohl es bei weitem der komplexeste und umfangreichste Teil des Codes ist: Sie müssen lediglich den Eingabeparameter einrichten . Das Schreiben von Tests für und ist etwas schwieriger (obwohl die Funktionen selbst fast trivial sind), da Sie vor und nach dem Aufrufen der Funktion Dinge im Dateisystem erstellen und / oder lesen müssen. Im Idealfall machen Sie solche Funktionen so einfach, dass Sie sich wohl fühlen, wenn Sie sie nicht testen, und sparen sich so viel Aufwand.convert()write_file()convert()read_file()write_file()

Der Unterschied zwischen Python und Haskell besteht darin, dass Haskell über eine Typprüfung verfügt, mit der nachgewiesen werden kann, dass Funktionen keine Nebenwirkungen haben. In Python müssen Sie , dass niemand hoffen , zufällig in einer Datei-Lesen oder -writing Funktion in fallen gelassen convert()(etwa read_config_file()). Wenn Sie in Haskell convert :: String -> Stringohne IOMonade deklarieren oder ähnliches, garantiert die Typprüfung, dass dies eine reine Funktion ist, die nur auf ihren Eingabeparametern und nichts anderem beruht. Wenn jemand versucht, Änderungen vorzunehmen convert, um eine Konfigurationsdatei zu lesen, werden schnell Compilerfehler angezeigt, die darauf hinweisen, dass sie die Reinheit der Funktion beeinträchtigen würden. (Und hoffentlich wären sie vernünftig genug, um read_config_filedas convertErgebnis zu verlassen und es weiterzugeben convert, um die Reinheit zu bewahren.)

Curt J. Sampson
quelle