Ich stoße oft auf folgende Aussagen / Argumente:
- Reine funktionale Programmiersprachen lassen keine Nebenwirkungen zu (und sind daher in der Praxis wenig nützlich, da jedes nützliche Programm Nebenwirkungen hat, z. B. wenn es mit der Außenwelt interagiert).
- Reine funktionale Programmiersprachen erlauben es nicht, ein Programm zu schreiben, das den Status beibehält (was das Programmieren sehr umständlich macht, da in vielen Anwendungen der Status benötigt wird).
Ich bin kein Experte für funktionale Sprachen, aber hier ist, was ich bisher über diese Themen verstanden habe.
In Bezug auf Punkt 1 können Sie in rein funktionalen Sprachen mit der Umgebung interagieren, müssen jedoch den Code (die Funktionen), der Nebenwirkungen hervorruft, explizit markieren (z. B. in Haskell durch monadische Typen). Soweit ich weiß, sollte es auch möglich sein, nach Nebenwirkungen zu rechnen (Daten destruktiv zu aktualisieren) (mit monadischen Typen?), Auch wenn dies nicht die bevorzugte Arbeitsweise ist.
In Bezug auf Punkt 2 können Sie, soweit ich weiß, den Zustand darstellen, indem Sie Werte durch mehrere Rechenschritte (in Haskell wiederum unter Verwendung monadischer Typen) verteilen. Ich habe jedoch keine praktischen Erfahrungen damit, und mein Verständnis ist ziemlich vage.
Sind die beiden obigen Aussagen in irgendeiner Weise richtig oder sind sie nur Missverständnisse über rein funktionale Sprachen? Wenn es sich um Missverständnisse handelt, wie sind sie entstanden? Könnten Sie einen (möglicherweise kleinen) Codeausschnitt schreiben, der die idiomatische Methode von Haskell veranschaulicht, um (1) Nebenwirkungen und (2) eine Berechnung mit state zu implementieren?
quelle
Antworten:
Für die Zwecke dieser Antwort definiere ich "rein funktionale Sprache" als eine funktionale Sprache, in der Funktionen referenziell transparent sind, dh der mehrfache Aufruf derselben Funktion mit denselben Argumenten führt immer zu denselben Ergebnissen. Dies ist meines Erachtens die übliche Definition einer rein funktionalen Sprache.
Der einfachste Weg, um referenzielle Transparenz zu erreichen, wäre in der Tat, Nebenwirkungen zu verbieten, und es gibt tatsächlich Sprachen, in denen dies der Fall ist (meistens domänenspezifische). Es ist jedoch sicherlich nicht der einzige Weg und die meisten rein funktionalen Allzwecksprachen (Haskell, Clean, ...) lassen Nebenwirkungen zu.
Zu sagen, dass eine Programmiersprache ohne Nebenwirkungen in der Praxis wenig nützlich ist, ist meiner Meinung nach nicht wirklich fair - sicherlich nicht für domänenspezifische Sprachen, aber selbst für Mehrzwecksprachen kann eine Sprache durchaus nützlich sein, ohne Nebenwirkungen zu verursachen . Vielleicht nicht für Konsolenanwendungen, aber ich denke, dass GUI-Anwendungen ohne Nebenwirkungen implementiert werden können, zum Beispiel im funktionalen reaktiven Paradigma.
Das ist ein bisschen zu einfach. Nur ein System zu haben, in dem nebenwirkende Funktionen als solche gekennzeichnet werden müssen (ähnlich wie die Konstanz in C ++, aber mit allgemeinen Nebenwirkungen), reicht nicht aus, um referenzielle Transparenz zu gewährleisten. Sie müssen sicherstellen, dass ein Programm eine Funktion niemals mehrmals mit denselben Argumenten aufrufen und unterschiedliche Ergebnisse erzielen kann. Sie könnten das entweder tun, indem Sie Dinge wie machen
readLine
Seien Sie etwas, das keine Funktion ist (das ist es, was Haskell mit der IO-Monade macht), oder Sie könnten es unmöglich machen, nebenwirkende Funktionen mehrmals mit demselben Argument aufzurufen (das ist es, was Clean tut). Im letzteren Fall würde der Compiler sicherstellen, dass Sie jedes Mal, wenn Sie eine Nebenwirkungsfunktion aufrufen, ein neues Argument eingeben und jedes Programm ablehnen, bei dem Sie dasselbe Argument zweimal an eine Nebenwirkungsfunktion übergeben.Auch hier könnte eine rein funktionale Sprache einen veränderlichen Zustand sehr wohl verbieten, aber es ist sicherlich möglich, rein zu sein und noch einen veränderlichen Zustand zu haben, wenn Sie sie auf die gleiche Weise implementieren, wie ich oben mit Nebenwirkungen beschrieben habe. Wirklich veränderlicher Zustand ist nur eine andere Form von Nebenwirkungen.
Das heißt, funktionale Programmiersprachen entmutigen definitiv den veränderlichen Zustand - vor allem die reinen. Und ich denke nicht, dass das Programmieren umständlich ist - ganz im Gegenteil. Manchmal (aber nicht allzu oft) kann ein veränderlicher Zustand nicht vermieden werden, ohne an Leistung oder Klarheit zu verlieren (weshalb Sprachen wie Haskell über Funktionen für einen veränderlichen Zustand verfügen), aber meistens ist dies möglich.
Ich denke, viele Leute lesen einfach "eine Funktion muss dasselbe Ergebnis liefern, wenn sie mit denselben Argumenten aufgerufen wird" und schließen daraus, dass es nicht möglich ist, so etwas
readLine
oder Code zu implementieren , der einen veränderlichen Zustand beibehält. Sie sind sich also einfach nicht der "Cheats" bewusst, mit denen rein funktionale Sprachen diese Dinge einführen können, ohne die referentielle Transparenz zu brechen.Auch ein veränderlicher Zustand ist in funktionalen Sprachen stark entmutigend, so dass es kein großer Sprung ist anzunehmen, dass er in rein funktionalen überhaupt nicht zulässig ist.
Hier ist eine Anwendung in Pseudo-Haskell, die den Benutzer nach einem Namen fragt und ihn begrüßt. Pseudo-Haskell ist eine Sprache, die ich gerade erfunden habe und die Haskells IO-System hat, aber konventionellere Syntax, beschreibendere Funktionsnamen und keine
do
-Notation verwendet (da dies nur davon ablenken würde, wie genau die IO-Monade funktioniert):Der Hinweis hier ist, dass
readLine
es sich um einen Wert vom TypIO<String>
undcomposeMonad
eine Funktion handelt, die ein Argument vom TypIO<T>
(für einen bestimmten TypT
) und ein anderes Argument, das eine Funktion ist, die ein Argument vom TypT
annimmt und einen Wert vom TypIO<U>
(für einen bestimmten TypU
) zurückgibt .print
ist eine Funktion, die eine Zeichenfolge akzeptiert und einen Wert vom Typ zurückgibtIO<void>
.Ein Wert vom Typ
IO<A>
ist ein Wert, der eine bestimmte Aktion "codiert", die einen Wert vom Typ erzeugtA
.composeMonad(m, f)
Erzeugt einen neuenIO
Wert, der die Aktion vonm
gefolgt von der Aktion von codiert.f(x)
Dabeix
handelt es sich um den Wert, der durch Ausführen der Aktion von erzeugt wirdm
.Der veränderbare Zustand würde folgendermaßen aussehen:
Hier
mutableVariable
ist eine Funktion, die einen beliebigen Wert annimmtT
und a erzeugtMutableVariable<T>
. Die FunktiongetValue
nimmtMutableVariable
und gibt eine zurückIO<T>
, die ihren aktuellen Wert erzeugt.setValue
Nimmt einMutableVariable<T>
und einT
und gibt ein zurückIO<void>
, das den Wert festlegt.composeVoidMonad
ist dasselbe wie mit dercomposeMonad
Ausnahme, dass das erste Argument ein Argument istIO
, das keinen sinnvollen Wert erzeugt, und das zweite Argument eine andere Monade ist, keine Funktion, die eine Monade zurückgibt.In Haskell gibt es etwas syntaktischen Zucker, der diese ganze Tortur weniger schmerzhaft macht, aber es ist immer noch offensichtlich, dass der veränderbare Zustand etwas ist, das die Sprache nicht wirklich von Ihnen verlangt.
quelle
counter
, dhincreaseCounter(counter)
?main
diejenige ist, die tatsächlich ausgeführt wird. Anders als die Rückgabe einer E / A vonmain
dort gibt es keine Möglichkeit,IO
Aktionen auszuführen (ohne schrecklich böse Funktionen zu verwenden, dieunsafe
in ihrem Namen enthalten sind).IO
Werte. Ich habe nicht verstanden, ob er sich auf Pattern Matching bezieht, dh auf die Tatsache, dass man Werte eines algebraischen Datentyps dekonstruieren kann, aber man kann Pattern Matching nicht verwenden, um dies mitIO
Werten zu tun .IMHO sind Sie verwirrt, weil es einen Unterschied zwischen einer reinen Sprache und einer reinen Funktion gibt . Beginnen wir mit der Funktion. Eine Funktion ist rein, wenn sie (bei gleicher Eingabe) immer den gleichen Wert zurückgibt und keine beobachtbaren Nebenwirkungen verursacht. Typische Beispiele sind mathematische Funktionen wie f (x) = x * x. Betrachten Sie nun eine Implementierung dieser Funktion. Es wäre in den meisten Sprachen rein, auch in solchen, die im Allgemeinen nicht als reine Funktionssprachen gelten, z. B. ML. Sogar eine Java- oder C ++ - Methode mit diesem Verhalten kann als rein betrachtet werden.
Was ist also eine reine Sprache? Streng genommen könnte man erwarten, dass Sie mit einer reinen Sprache keine Funktionen ausdrücken können, die nicht rein sind. Nennen wir dies die idealistische Definition einer reinen Sprache. Ein solches Verhalten ist sehr wünschenswert. Warum? Das Schöne an einem Programm, das nur aus reinen Funktionen besteht, ist, dass Sie die Funktionsanwendung durch ihren Wert ersetzen können, ohne die Bedeutung des Programms zu ändern. Das macht es sehr einfach, über Programme nachzudenken, denn sobald Sie das Ergebnis kennen, können Sie vergessen, wie es berechnet wurde. Die Reinheit kann es dem Compiler auch ermöglichen, bestimmte aggressive Optimierungen durchzuführen.
Was ist, wenn Sie einen internen Zustand benötigen? Sie können den Zustand in einer reinen Sprache simulieren, indem Sie den Zustand vor der Berechnung als Eingabeparameter und den Zustand nach der Berechnung als Teil des Ergebnisses hinzufügen. Stattdessen
Int -> Bool
bekommst du sowasInt -> State -> (Bool, State)
. Sie machen die Abhängigkeit einfach explizit (was in jedem Programmierparadigma als gute Praxis angesehen wird). Übrigens gibt es eine Monade, mit der sich solche Zustandsnachahmungsfunktionen besonders elegant zu größeren Zustandsnachahmungsfunktionen kombinieren lassen. Auf diese Weise können Sie definitiv den Status in einer reinen Sprache "aufrechterhalten". Aber Sie müssen es explizit machen.Heißt das, ich kann mit der Außenwelt interagieren? Schließlich muss ein nützliches Programm mit der realen Welt interagieren, um nützlich zu sein. Aber Input und Output sind offensichtlich nicht rein. Das Schreiben eines bestimmten Bytes in eine bestimmte Datei ist möglicherweise zum ersten Mal in Ordnung. Wenn Sie dieselbe Operation jedoch ein zweites Mal ausführen, wird möglicherweise ein Fehler zurückgegeben, da der Datenträger voll ist. Offensichtlich gibt es keine reine Sprache (im idealistischen Sinne), die in eine Datei schreiben kann.
Wir stehen also vor einem Dilemma. Wir wollen hauptsächlich reine Funktionen, aber einige Nebenwirkungen sind unbedingt erforderlich und diese sind nicht rein. Eine realistische Definition einer reinen Sprache wäre nun, dass es Mittel geben muss, um die reinen Teile von den anderen Teilen zu trennen. Der Mechanismus muss sicherstellen, dass keine unreine Operation in die Reinteile eindringt.
In Haskell erfolgt dies mit dem IO-Typ. Sie können ein E / A-Ergebnis nicht zerstören (ohne unsichere Mechanismen). Sie können E / A-Ergebnisse daher nur mit Funktionen verarbeiten, die im E / A-Modul selbst definiert sind. Glücklicherweise gibt es sehr flexible Kombinatoren, mit denen Sie ein E / A-Ergebnis nehmen und in einer Funktion verarbeiten können, solange diese Funktion ein anderes E / A-Ergebnis zurückgibt. Dieser Kombinator heißt bind (oder
>>=
) und hat den TypIO a -> (a -> IO b) -> IO b
. Wenn Sie dieses Konzept verallgemeinern, gelangen Sie zur Monadenklasse, und IO ist zufällig eine Instanz davon.quelle
unsafe
ihrem Namen ignoriert ) Ihrer idealistischen Definition nicht entspricht. Es gibt keine unreinen Funktionen in Haskell (wieder ignorierenunsafePerformIO
und co.).readFile
undwriteFile
gibtIO
bei gleichen Argumenten immer den gleichen Wert zurück. Also zB die beiden Codefragmentelet x = writeFile "foo.txt" "bar" in x >> x
undwriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"
machen das selbe.IO Something
? In diesem Fall ist es durchaus möglich, eine E / A-Funktion zweimal mit demselben Argument aufzurufen:putStrLn "hello" >> putStrLn "hello"
- Hier müssen beide AufrufeputStrLn
dasselbe Argument haben. Das ist natürlich kein Problem, denn wie ich bereits sagte, führen beide Aufrufe zu demselben E / A-Wert.writeFile "foo.txt" "bar"
kann keinen Fehler verursachen, da das Auswerten des Funktionsaufrufs die Aktion nicht ausführt. Wenn Sie in meinem vorherigen Beispiel sagen, dass die Version mitlet
nur eine Möglichkeit hat, einen E / A-Fehler zu verursachen, während die Version ohnelet
zwei Möglichkeiten hat, liegen Sie falsch. Beide Versionen bieten zwei Möglichkeiten für einen E / A-Fehler. Da dielet
Version den AufrufwriteFile
nur einmallet
auswertet, während die Version ohne zweimal auswertet, können Sie sehen, dass es egal ist, wie oft die Funktion aufgerufen wird. Es ist nur wichtig, wie oft die resultierende ...putStrLn
Funktion akzeptiert genau ein Argument vom TypString
. Wenn Sie mir nicht glauben, schauen Sie sich seine Art:String -> IO ()
. Es werden sicherlich keine Argumente vom Typ verwendetIO
- es wird ein Wert dieses Typs erzeugt.