In vielen Artikeln, die die Vorteile der funktionalen Programmierung beschreiben, habe ich funktionale Programmiersprachen wie Haskell, ML, Scala oder Clojure gesehen, die als "deklarative Sprachen" bezeichnet werden und sich von imperativen Sprachen wie C / C ++ / C # / Java unterscheiden. Meine Frage ist, warum funktionale Programmiersprachen deklarativ und nicht imperativ sind.
Eine häufig anzutreffende Erklärung für die Unterschiede zwischen deklarativer und imperativer Programmierung ist, dass Sie bei der imperativen Programmierung dem Computer sagen, wie etwas zu tun ist, und nicht, was in deklarativen Sprachen zu tun ist. Das Problem, das ich mit dieser Erklärung habe, ist, dass Sie ständig beides in allen Programmiersprachen tun. Selbst wenn Sie zur Baugruppe der untersten Ebene gehen und dem Computer immer noch sagen, was zu tun ist, weisen Sie die CPU an, zwei Zahlen hinzuzufügen. Sie weisen sie nicht an, wie das Hinzufügen durchgeführt werden soll. Wenn wir uns dem anderen Ende des Spektrums zuwenden, einer hochgradig reinen funktionalen Sprache wie Haskell, sagen Sie dem Computer tatsächlich, wie eine bestimmte Aufgabe zu erfüllen ist. Das ist, was Ihr Programm ist eine Folge von Anweisungen, um eine bestimmte Aufgabe zu erreichen, die der Computer nicht allein zu erreichen weiß. Ich verstehe, dass Sprachen wie Haskell, Clojure usw. offensichtlich höher sind als C / C ++ / C # / Java und Features wie Lazy Evaluation, unveränderbare Datenstrukturen, anonyme Funktionen, Currying, persistente Datenstrukturen usw. bieten funktionale Programmierung möglich und effizient, aber ich würde sie nicht als deklarative Sprachen klassifizieren.
Eine reine deklarative Sprache wäre für mich eine, die ausschließlich aus Deklarationen besteht. Ein Beispiel für eine solche Sprache wäre CSS (Ja, ich weiß, CSS wäre technisch gesehen keine Programmiersprache). CSS enthält nur Stildeklarationen, die von HTML und Javascript der Seite verwendet werden. CSS kann nichts anderes als Deklarationen erstellen, es kann keine Klassenfunktionen erstellen, dh Funktionen, die den anzuzeigenden Stil auf der Grundlage einiger Parameter bestimmen, Sie können keine CSS-Skripten ausführen usw. Das beschreibt für mich eine deklarative Sprache (ich habe nicht deklarativ gesagt) Programmiersprache ).
Aktualisieren:
Ich habe in letzter Zeit mit Prolog herumgespielt und für mich ist Prolog die Programmiersprache, die einer vollständig deklarativen Sprache am nächsten kommt (zumindest meiner Meinung nach), wenn es nicht die einzige vollständig deklarative Programmiersprache ist. Die Programmierung in Prolog wird durch Deklarationen ausgeführt, die entweder eine Tatsache (eine Prädikatfunktion, die für eine bestimmte Eingabe true zurückgibt) oder eine Regel (eine Prädikatfunktion, die für eine gegebene Bedingung / ein gegebenes Muster basierend auf den Eingaben true zurückgibt), Regeln angeben werden mit einer Mustervergleichstechnik definiert. Um irgendetwas in Prolog zu tun, fragen Sie die Wissensbasis ab, indem Sie eine oder mehrere Eingaben eines Prädikats durch eine Variable ersetzen, und Prolog versucht, Werte für die Variable (n) zu finden, für die das Prädikat erfolgreich ist.
Mein Punkt im Prolog ist, dass es keine zwingenden Anweisungen gibt. Sie sagen dem Computer im Grunde, was er weiß, und fragen dann nach dem Wissen. In funktionalen Programmiersprachen geben Sie weiterhin Anweisungen, dh nehmen Sie einen Wert, rufen Sie die Funktion X auf und fügen Sie 1 hinzu usw., auch wenn Sie nicht direkt die Speicherorte manipulieren oder die Berechnung Schritt für Schritt ausschreiben. Ich würde nicht sagen, dass die Programmierung in Haskell, ML, Scala oder Clojure in diesem Sinne aussagekräftig ist, auch wenn ich mich irre. Ist richtige, wahre, reine funktionale Programmierung deklarativ in dem Sinne, wie ich es oben beschrieben habe.
(let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))
(Hoffe du hast das verstanden, Clojure). Meine ursprüngliche Frage ist, was dies von diesem Int unterscheidet.x = 1; x += 2; x *= x; return x;
Meiner Meinung nach ist es größtenteils dasselbe.Antworten:
Sie scheinen eine Grenze zwischen dem Erklären von Dingen und dem Anweisen einer Maschine zu ziehen. Es gibt keine solche schnelle Trennung. Da es sich bei der Maschine, die in die Imperative Programmierung eingewiesen wird, nicht unbedingt um physische Hardware handelt, gibt es viel Interpretationsfreiheit. Fast alles kann als explizites Programm für die richtige abstrakte Maschine angesehen werden. Sie könnten beispielsweise CSS als eine Sprache auf höherer Ebene für die Programmierung einer Maschine betrachten, die in erster Linie Selektoren auflöst und Attribute der so ausgewählten DOM-Objekte festlegt.
Die Frage ist, ob eine solche Perspektive sinnvoll ist und umgekehrt, inwieweit die Reihenfolge der Anweisungen einer Deklaration des zu berechnenden Ergebnisses ähnelt . Für CSS ist die deklarative Perspektive eindeutig nützlicher. Für C ist die imperative Perspektive eindeutig vorherrschend. Was Sprachen wie Haskell betrifft, na ja ...
Die Sprache hat eine konkrete Semantik angegeben. Das heißt, man kann das Programm natürlich als eine Kette von Operationen interpretieren. Es ist nicht einmal zu aufwendig, die primitiven Operationen so auszuwählen, dass sie gut auf Standardhardware abgebildet werden (das ist es, was die STG-Maschine und andere Modelle tun).
Die Art und Weise, wie Haskell-Programme geschrieben werden, kann jedoch häufig sinnvoll als Beschreibung des zu berechnenden Ergebnisses gelesen werden. Nehmen wir zum Beispiel das Programm zur Berechnung der Summe der ersten N Fakultäten:
Sie können dies deaktivieren und als eine Folge von STG-Operationen lesen, aber viel natürlicher ist es, es als Beschreibung des Ergebnisses zu lesen (was meiner Meinung nach eine nützlichere Definition der deklarativen Programmierung ist als "was zu berechnen ist"): Das Ergebnis ist die Summe der Produkte von
[1..i]
for alli
= 0,…, n. Und das ist weitaus aussagekräftiger als fast jedes C-Programm oder jede C-Funktion.quelle
map (sum_of_fac . read) (lines fileContent)
. Natürlich kommt irgendwann I / O ins Spiel, aber wie gesagt, es ist ein Kontinuum. Teile von Haskell-Programmen sind sicherx + y
load x; load y; add;
Die Grundeinheit eines imperativen Programms ist die Aussage . Anweisungen werden auf ihre Nebenwirkungen hin ausgeführt. Sie ändern den Status, den sie erhalten. Eine Folge von Anweisungen ist eine Folge von Befehlen. Der Programmierer gibt die genaue Reihenfolge für die Durchführung der Berechnung an. Das ist es, was die Leute damit meinen, dem Computer zu sagen, wie er es machen soll.
Die Grundeinheit eines deklarativen Programms ist die Ausdruck . Ausdrücke haben keine Nebenwirkungen. Sie geben eine Beziehung zwischen einer Eingabe und einer Ausgabe an, wodurch eine neue und separate Ausgabe von ihrer Eingabe erstellt wird, anstatt ihren Eingabezustand zu ändern. Eine Folge von Ausdrücken ist bedeutungslos, ohne dass ein enthaltener Ausdruck die Beziehung zwischen ihnen angibt. Der Programmierer gibt die Beziehungen zwischen Daten an, und das Programm leitet die Reihenfolge ab, in der die Berechnung aus diesen Beziehungen durchgeführt wird. Das ist es, was die Leute damit meinen, dem Computer zu sagen, was er tun soll.
Imperative Sprachen haben Ausdrücke, aber ihre Hauptmittel, um Dinge zu erledigen, sind Aussagen. Ebenso haben deklarative Sprachen einige Ausdrücke mit einer Semantik, die Aussagen ähnelt, wie Monadensequenzen in Haskells
do
Notation, aber im Kern sind sie ein einziger großer Ausdruck. So können Anfänger Code schreiben, der sehr imperativ aussieht, aber die wahre Kraft der Sprache kommt, wenn Sie diesem Paradigma entkommen.quelle
foreach
.Das eigentliche definierende Merkmal, das deklarative von imperativen Programmen unterscheidet, ist der deklarative Stil, mit dem Sie keine sequenzierten Anweisungen geben. auf der unteren Ebene arbeitet die CPU ja so, aber das ist ein Anliegen des Compilers.
Sie schlagen vor, CSS sei eine "deklarative Sprache", ich würde es überhaupt nicht als Sprache bezeichnen. Es ist ein Datenstrukturformat wie JSON, XML, CSV oder INI, nur ein Format zum Definieren von Daten, die einem Interpreter irgendeiner Art bekannt sind.
Einige interessante Nebenwirkungen treten auf, wenn Sie den Zuweisungsoperator aus einer Sprache entfernen. Dies ist die eigentliche Ursache für den Verlust all dieser wichtigen Anweisungen für Schritt 1, Schritt 2 und Schritt 3 in deklarativen Sprachen.
Was machen Sie mit dem Zuweisungsoperator in einer Funktion? Sie legen Zwischenschritte an. Das ist der springende Punkt, der Zuweisungsoperator wird verwendet, um Daten schrittweise zu ändern. Sobald Sie keine Daten mehr ändern können, verlieren Sie alle diese Schritte und erhalten:
Jede Funktion hat nur eine Anweisung, wobei diese Anweisung die einzelne Deklaration ist
Nun gibt es viele Möglichkeiten, eine einzelne Aussage wie eine viele Aussage aussehen zu lassen, aber das ist nur ein Trick, zum Beispiel:
1 + 3 + (2*4) + 8 - (7 / (8*3))
Dies ist eindeutig eine einzelne Aussage, aber wenn Sie sie schreiben als ...Es kann den Anschein einer Abfolge von Operationen annehmen, die es dem Gehirn erleichtern können, die gewünschte Zersetzung zu erkennen, die der Autor beabsichtigt hatte. Ich mache das oft in C # mit Code wie ..
Dies ist eine einzelne Aussage, zeigt aber den Anschein, als würde sie in viele zerlegt - es gibt jedoch keine Zuordnungen.
Beachten Sie auch, dass bei beiden obigen Methoden die Art und Weise, wie der Code tatsächlich ausgeführt wird, nicht in der Reihenfolge ist, in der Sie ihn sofort eingelesen haben. weil dieser Code sagt, was zu tun ist, aber nicht vorschreibt, wie . Beachten Sie, dass die obige einfache Arithmetik offensichtlich nicht in der Reihenfolge ausgeführt wird, in der sie von links nach rechts geschrieben ist, und in dem obigen C # -Beispiel sind diese drei Methoden tatsächlich alle re-entrant und werden nicht vollständig in der Reihenfolge ausgeführt. Der Compiler generiert tatsächlich Code das wird tun, was diese Aussage will , aber nicht unbedingt wie Sie annehmen.
Ich glaube, an diesen Ansatz der deklarativen Codierung ohne Zwischenschritte zu gewöhnen, ist der schwierigste Teil von allem. und warum Haskell so knifflig ist, weil es nur wenige Sprachen wirklich verbieten, wie es Haskell tut. Sie müssen mit interessanten Gymnastikübungen beginnen, um einige Aufgaben zu erledigen, die Sie normalerweise mit Hilfe von Zwischenvariablen ausführen würden, z. B. Summieren.
Wenn Sie in einer deklarativen Sprache möchten, dass eine Zwischenvariable etwas tut, müssen Sie sie als Parameter an eine Funktion übergeben, die das tut. Deshalb wird Rekursion so wichtig.
sum xs = (head xs) + sum (tail xs)
Sie können keine
resultSum
Variable erstellen und während des Durchschleifens hinzufügen.xs
Sie müssen lediglich den ersten Wert nehmen und zu der Summe aller anderen Werte hinzufügen. Um auf alle anderen Werte zuzugreifen, müssen Sie diesexs
an eine Funktion übergeben,tail
weil Sie können nicht einfach eine Variable für xs erstellen und den Kopf abwerfen, was ein zwingender Ansatz wäre. (Ja, ich weiß, dass Sie eine Destrukturierung verwenden können, aber dieses Beispiel soll veranschaulichend sein.)quelle
x = 1 + 2
dannx = 3
undx = 4 - 1
. In sequenzierten Anweisungen werden Anweisungen definiert. Wennx = (y = 1; z = 2 + y; return z;)
Sie also nicht mehr über etwas Formbares verfügen, kann der Compiler dies auf verschiedene Arten tun. Der Compiler muss jedoch unbedingt das tun, was Sie dort angegeben haben, da der Compiler nicht alle Nebenwirkungen Ihrer Anweisungen kennen kann. ' t ändern sie.Ich weiß, dass ich zu spät zur Party komme, aber ich hatte neulich eine Offenbarung, also geht es los ...
Ich denke, Kommentare über funktionale, unveränderliche und keine Nebenwirkungen verfehlen das Ziel, wenn es darum geht, den Unterschied zwischen deklarativ und imperativ zu erklären oder zu erklären, was deklarative Programmierung bedeutet. Wie Sie in Ihrer Frage erwähnen, ist das ganze "Was zu tun ist" im Vergleich zu "Wie es zu tun ist" zu vage und erklärt es auch nicht wirklich.
Nehmen wir den einfachen Code
a = b + c
als Grundlage und betrachten die Anweisung in einigen verschiedenen Sprachen, um die Idee zu erhalten:Wenn wir
a = b + c
in einer imperativen Sprache wie C schreiben , weisen wir der Variablen den aktuellen Wert vonb + c
zua
und nichts weiter. Wir machen keine grundsätzlichen Aussagen darüber, wasa
ist. Vielmehr führen wir einfach einen Schritt in einem Prozess aus.Wenn wir schreiben
a = b + c
in einer deklarativen Sprache, wie Microsoft Excel, (ja, Excel ist eine Programmiersprache und wahrscheinlich die deklarative von allen,) sind wir eine Beziehung zu behaupten zwischena
,b
undc
so , dass es immer , dass der Falla
ist die Summe aus die anderen zwei. Es ist kein Schritt in einem Prozess, es ist eine Invariante, eine Garantie, eine Erklärung der Wahrheit.Auch funktionale Sprachen sind deklarativ, aber fast zufällig. In Haskel wird zum Beispiel
a = b + c
auch eine unveränderliche Beziehung behauptet, aber nur weilb
undc
unveränderlich sind.Also ja, wenn Objekte unveränderlich sind und Funktionen frei von Nebenwirkungen sind, wird Code deklarativ (auch wenn er identisch mit imperativem Code aussieht), aber es geht nicht darum. Es geht auch nicht darum, eine Zuordnung zu vermeiden. Der Sinn von deklarativem Code besteht darin, grundlegende Aussagen über Beziehungen zu treffen.
quelle
Sie haben Recht , dass es zwischen sagen , den Computer keine klare Unterscheidung ist , was zu tun ist und wie es zu tun.
Auf einer Seite des Spektrums denken Sie jedoch fast ausschließlich darüber nach, wie Sie das Gedächtnis manipulieren können. Das heißt, um ein Problem zu lösen, präsentieren Sie es dem Computer in einer Form wie "Setzen Sie diesen Speicherort auf x, setzen Sie diesen Speicherort auf y, springen Sie zum Speicherort z ...", und dann haben Sie es irgendwie das Ergebnis in einem anderen Speicherort.
In verwalteten Sprachen wie Java, C # usw. haben Sie keinen direkten Zugriff mehr auf den Hardwarespeicher. Der imperative Programmierer befasst sich jetzt mit statischen Variablen, Referenzen oder Feldern von Klasseninstanzen, die alle zu einem gewissen Grad Absraktionen für Speicherstellen sind.
In Sprachen wie Haskell, OTOH, ist die Erinnerung vollständig verschwunden. Es ist einfach nicht der Fall, dass in
Es müssen zwei Speicherzellen vorhanden sein, die die Argumente a und b enthalten, und eine weitere, die das Zwischenergebnis y enthält. Natürlich könnte ein Compiler-Backend endgültigen Code ausgeben, der auf diese Weise funktioniert (und in gewissem Sinne muss dies auch so sein, solange die Zielarchitektur eine v. Neumann-Maschine ist).
Der Punkt ist jedoch, dass wir die v. Neumann-Architektur nicht verinnerlichen müssen, um die obige Funktion zu verstehen. Wir brauchen auch keinen modernen Computer, um ihn zu betreiben. Es wäre einfach, Programme in einer reinen FP-Sprache in eine hypothetische Maschine zu übersetzen, die beispielsweise auf der Grundlage des SKI-Kalküls arbeitet. Versuchen Sie es jetzt gleich mit einem C-Programm!
Das ist meiner Meinung nach nicht stark genug. Auch ein C-Programm ist nur eine Folge von Deklarationen. Ich denke, wir müssen die Erklärungen weiter qualifizieren. Sagen sie uns zum Beispiel, was etwas ist (deklarativ) oder was es tut (imperativ)?
quelle