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.
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).
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.
In Bezug auf Punkt 1 können Sie mit der Umgebung in rein funktionalen Sprachen interagieren, aber Sie müssen den Code (Funktionen), der sie einführt, explizit markieren (z. B. in Haskell durch monadische Typen).
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 machenreadLine
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.
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).
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.
Wenn es sich um Missverständnisse handelt, wie sind sie entstanden?
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.
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?
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):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
Der Hinweis hier ist, dass readLine
es sich um einen Wert vom Typ IO<String>
und composeMonad
eine Funktion handelt, die ein Argument vom Typ IO<T>
(für einen bestimmten Typ T
) und ein anderes Argument, das eine Funktion ist, die ein Argument vom Typ T
annimmt und einen Wert vom Typ IO<U>
(für einen bestimmten Typ U
) zurückgibt . print
ist eine Funktion, die eine Zeichenfolge akzeptiert und einen Wert vom Typ zurückgibt IO<void>
.
Ein Wert vom Typ IO<A>
ist ein Wert, der eine bestimmte Aktion "codiert", die einen Wert vom Typ erzeugt A
. composeMonad(m, f)
Erzeugt einen neuen IO
Wert, der die Aktion von m
gefolgt von der Aktion von codiert. f(x)
Dabei x
handelt es sich um den Wert, der durch Ausführen der Aktion von erzeugt wird m
.
Der veränderbare Zustand würde folgendermaßen aussehen:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Hier mutableVariable
ist eine Funktion, die einen beliebigen Wert annimmt T
und a erzeugt MutableVariable<T>
. Die Funktion getValue
nimmt MutableVariable
und gibt eine zurück IO<T>
, die ihren aktuellen Wert erzeugt. setValue
Nimmt ein MutableVariable<T>
und ein T
und gibt ein zurück IO<void>
, das den Wert festlegt. composeVoidMonad
ist dasselbe wie mit der composeMonad
Ausnahme, dass das erste Argument ein Argument ist IO
, 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.