Wenn funktionale Programmiersprachen keinen Status speichern können, wie machen sie dann einige einfache Dinge wie das Lesen von Eingaben eines Benutzers (ich meine, wie "speichern" sie diese) oder das Speichern von Daten für diese Angelegenheit?
Wie Sie gesehen haben, hat die funktionale Programmierung keinen Status - aber das bedeutet nicht, dass sie keine Daten speichern kann. Der Unterschied besteht darin, dass ich eine (Haskell) -Anweisung nach dem Vorbild von schreibe
let x = func value 3.14 20 "random"
in ...
Mir wird garantiert, dass der Wert von x
immer gleich ist in ...
: nichts kann es möglicherweise ändern. Wenn ich eine Funktion habe f :: String -> Integer
(eine Funktion, die eine Zeichenfolge verwendet und eine Ganzzahl zurückgibt), kann ich sicher sein, dass f
das Argument nicht geändert oder globale Variablen geändert oder Daten in eine Datei geschrieben werden usw. Wie sepp2k in einem Kommentar oben sagte, ist diese Nichtveränderlichkeit sehr hilfreich, um über Programme nachzudenken: Sie schreiben Funktionen, die Ihre Daten falten, spindeln und verstümmeln, geben neue Kopien zurück, damit Sie sie miteinander verketten können, und Sie können sicher sein, dass keine vorhanden sind von diesen Funktionsaufrufen kann alles "schädliche" tun. Sie wissen, dass x
das immer so ist x
, und Sie müssen sich keine Sorgen machen, dass jemand x := foo bar
irgendwo zwischen der Erklärung von geschrieben hatx
und seine Verwendung, weil das unmöglich ist.
Was ist nun, wenn ich Eingaben von einem Benutzer lesen möchte? Wie KennyTM sagte, ist die Idee, dass eine unreine Funktion eine reine Funktion ist, die die ganze Welt als Argument weitergegeben hat und sowohl ihr Ergebnis als auch die Welt zurückgibt. Natürlich möchten Sie dies nicht wirklich tun: Zum einen ist es schrecklich klobig, und zum anderen, was passiert, wenn ich dasselbe Weltobjekt wiederverwenden? Das wird also irgendwie abstrahiert. Haskell behandelt es mit dem IO-Typ:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Dies sagt uns, dass main
es sich um eine E / A-Aktion handelt, die nichts zurückgibt. Das Ausführen dieser Aktion bedeutet, ein Haskell-Programm auszuführen. Die Regel lautet, dass E / A-Typen einer E / A-Aktion niemals entkommen können. In diesem Zusammenhang führen wir diese Aktion mit ein do
. Somit getLine
kehrt ein IO String
, die auf zwei Arten gedacht werden kann: Erstens, als eine Aktion , die, wenn sie ausgeführt wird , eine Zeichenfolge erzeugt; zweitens als eine Zeichenfolge, die von IO "verdorben" wird, da sie unrein erhalten wurde. Der erste ist korrekter, aber der zweite kann hilfreicher sein. Das <-
nimmt das String
heraus IO String
und speichert es in str
- aber da wir uns in einer E / A-Aktion befinden, müssen wir es wieder einpacken, damit es nicht "entkommen" kann. Die nächste Zeile versucht, eine Ganzzahl ( reads
) zu lesen und erfasst die erste erfolgreiche Übereinstimmung (fst . head
); das ist alles rein (kein IO), also geben wir ihm einen Namen mit let no = ...
. Wir können dann beide no
und str
in der verwenden ...
. Wir haben also unreine Daten (von getLine
in str
) und reine Daten ( let no = ...
) gespeichert .
Dieser Mechanismus für die Arbeit mit E / A ist sehr leistungsfähig: Mit ihm können Sie den reinen, algorithmischen Teil Ihres Programms von der unreinen Seite der Benutzerinteraktion trennen und dies auf Typebene erzwingen. Ihre minimumSpanningTree
Funktion kann möglicherweise an keiner anderen Stelle in Ihrem Code etwas ändern oder eine Nachricht an Ihren Benutzer schreiben und so weiter. Es ist sicher.
Dies ist alles, was Sie wissen müssen, um IO in Haskell verwenden zu können. Wenn das alles ist, was Sie wollen, können Sie hier aufhören. Aber wenn Sie verstehen wollen, warum das funktioniert, lesen Sie weiter. (Und beachten Sie, dass dieses Zeug spezifisch für Haskell ist - andere Sprachen wählen möglicherweise eine andere Implementierung.)
Das schien also wahrscheinlich ein kleiner Betrug zu sein, der dem reinen Haskell irgendwie Unreinheit verlieh. Aber es ist nicht so - es stellt sich heraus, dass wir den E / A-Typ vollständig in reinem Haskell implementieren können (solange wir den erhalten RealWorld
). Die Idee ist folgende: Eine E / A-Aktion IO type
ist dieselbe wie eine Funktion RealWorld -> (type, RealWorld)
, die die reale Welt übernimmt und sowohl ein Objekt vom Typ type
als auch das modifizierte zurückgibt RealWorld
. Wir definieren dann ein paar Funktionen, damit wir diesen Typ verwenden können, ohne verrückt zu werden:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
Die erste ermöglicht es uns, über E / A-Aktionen zu sprechen, die nichts bewirken: Es return 3
handelt sich um eine E / A-Aktion, die die reale Welt nicht abfragt und nur zurückkehrt 3
. Der >>=
Operator, ausgesprochen "bind", ermöglicht es uns, E / A-Aktionen auszuführen. Es extrahiert den Wert aus der E / A-Aktion, leitet ihn und die reale Welt durch die Funktion und gibt die resultierende E / A-Aktion zurück. Beachten Sie, dass dies >>=
unsere Regel erzwingt, dass die Ergebnisse von E / A-Aktionen niemals entkommen dürfen.
Wir können das Obige dann main
in die folgenden gewöhnlichen Funktionsanwendungen umwandeln:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Die Haskell-Laufzeit beginnt main
mit der Initiale RealWorld
und wir sind fertig! Alles ist rein, es hat nur eine ausgefallene Syntax.
[ Bearbeiten: Wie @Conal hervorhebt, ist dies nicht das, was Haskell verwendet, um E / A auszuführen . Dieses Modell bricht ab, wenn Sie Parallelität hinzufügen oder sich die Welt während einer E / A-Aktion auf irgendeine Weise ändern kann. Daher ist es für Haskell unmöglich, dieses Modell zu verwenden. Es ist nur für die sequentielle Berechnung genau. So kann es sein, dass Haskells IO ein bisschen ausweicht; Auch wenn es nicht so ist, ist es sicherlich nicht ganz so elegant. Siehe @ Conals Beobachtung, was Simon Peyton-Jones in Tackling the Awkward Squad [pdf] , Abschnitt 3.1 sagt ; er stellt vor, was in diesem Sinne einem alternativen Modell gleichkommen könnte, lässt es dann aber wegen seiner Komplexität fallen und geht einen anderen Weg.]
Dies erklärt wiederum (ziemlich genau), wie IO und die Veränderlichkeit im Allgemeinen in Haskell funktionieren. Wenn dies alles ist, was Sie wissen möchten, können Sie hier aufhören zu lesen. Wenn Sie eine letzte Dosis Theorie wünschen, lesen Sie weiter - aber denken Sie daran, dass wir zu diesem Zeitpunkt wirklich weit von Ihrer Frage entfernt sind!
Die letzte Sache: Es stellt sich heraus, dass diese Struktur - ein parametrischer Typ mit return
und >>=
- sehr allgemein ist. Es heißt Monade und do
Notation return
und >>=
arbeitet mit einem von ihnen. Wie Sie hier gesehen haben, sind Monaden nicht magisch; Alles, was magisch ist, ist, dass do
Blöcke zu Funktionsaufrufen werden. Der RealWorld
Typ ist der einzige Ort, an dem wir Magie sehen. Typen wie []
der Listenkonstruktor sind ebenfalls Monaden und haben nichts mit unreinem Code zu tun.
Sie wissen jetzt (fast) alles über das Konzept einer Monade (mit Ausnahme einiger Gesetze, die erfüllt sein müssen, und der formalen mathematischen Definition), aber Ihnen fehlt die Intuition. Es gibt eine lächerliche Anzahl von Monaden-Tutorials online; Ich mag dieses , aber Sie haben Optionen. Dies wird Ihnen jedoch wahrscheinlich nicht helfen . Der einzige wirkliche Weg, um die Intuition zu erlangen, besteht darin, sie zu verwenden und zum richtigen Zeitpunkt einige Tutorials zu lesen.
Sie benötigen diese Intuition jedoch nicht, um IO zu verstehen . Das allgemeine Verständnis von Monaden ist das i-Tüpfelchen, aber Sie können jetzt IO verwenden. Sie können es verwenden, nachdem ich Ihnen die erste main
Funktion gezeigt habe. Sie können IO-Code sogar so behandeln, als wäre er in einer unreinen Sprache! Aber denken Sie daran, dass es eine funktionale Repräsentation gibt: Niemand betrügt.
(PS: Entschuldigung für die Länge. Ich bin ein bisschen weit gegangen.)