Was macht funktionale Programmiersprachen deklarativ gegenüber imperativ?

17

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.

ALXGTV
quelle
Wo ist @JimmyHoffa, wenn du ihn brauchst?
2
1. C # und modernes C ++ sind sehr funktionale Sprachen. 2. Denken Sie an den Unterschied zwischen dem Schreiben von SQL und Java, um einen ähnlichen Zweck zu erreichen (möglicherweise eine komplexe Abfrage mit Verknüpfungen). Sie können sich auch
vorstellen
auch ist der Unterschied eher eine Frage des Stils als etwas klar definiertes ... es sei denn, Sie verwenden einen Prolog ...
AK_
1
Einer der Schlüssel zum Erkennen des Unterschieds ist die Verwendung eines Zuweisungsoperators im Vergleich zu einem Definitions- / Deklarationsoperator. In Haskell gibt es beispielsweise keinen Zuweisungsoperator . Das Verhalten dieser beiden Operatoren ist sehr unterschiedlich und zwingt Sie dazu, Ihren Code unterschiedlich zu gestalten.
Jimmy Hoffa
1
Leider kann ich das auch ohne den Zuweisungsoperator tun (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.
ALXGTV

Antworten:

16

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:

sum_of_fac n = sum [product [1..i] | i <- ..n]

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 all i= 0,…, n. Und das ist weitaus aussagekräftiger als fast jedes C-Programm oder jede C-Funktion.

Mathias Bynens
quelle
Der Grund, warum ich diese Frage gestellt habe, ist, dass viele Leute versuchen, die funktionale Programmierung zu fördern, als ein neues, eigenständiges Paradigma, das völlig von den Paradigmen von C / C ++ / C # / Java oder sogar Python getrennt ist, wenn die funktionale Programmierung in Wirklichkeit nur eine andere Form von höherer Ebene ist Imperative Programmierung.
ALXGTV
Um zu erklären, was ich hier meine, definieren Sie sum_of_fac, aber sum_of_fac ist für sich genommen so gut wie nutzlos, es sei denn, dies ist das Ergebnis Ihrer gesamten Anwendung Aufgabe, für die Ihr Programm entwickelt wurde.
ALXGTV
6
@ALXGTV Funktionale Programmierung ist ein ganz anderes Paradigma, auch wenn Sie es unbedingt lesen möchten (es ist eine sehr weit gefasste Klassifizierung, es gibt mehr als das Programmierparadigma). Und der andere Code, der auf diesem Code aufbaut, kann auch deklarativ gelesen werden: Zum Beispiel 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 sicher x + yload x; load y; add;
1
@ALXGTV Ihre Intuition ist richtig: Die deklarative Programmierung bietet "nur" eine höhere Abstraktionsebene für bestimmte wichtige Details. Ich würde behaupten, das ist eine große Sache!
Andres F.
1
@Brendan Ich denke nicht, dass funktionale Programmierung eine Teilmenge der imperativen Programmierung ist (noch ist Unveränderlichkeit eine zwingende Eigenschaft von FP). Es kann argumentiert werden, dass es sich bei reinen, statisch typisierten FPs um eine Obermenge der imperativen Programmierung handelt, bei der die Effekte in den Typen explizit sind. Sie kennen die ironische Behauptung, dass Haskell "die beste Imperativsprache der Welt" ist;)
Andres F.
21

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 doNotation, 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.

Karl Bielefeldt
quelle
1
Dies wäre eine perfekte Antwort, wenn sie ein Beispiel enthalten würde. Das beste Beispiel dafür, dass ich wüsste zu illustrieren deklarative vs. ist zwingend notwendig , Linq vs. foreach.
Robert Harvey,
7

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 ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

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 ..

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

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 resultSumVariable erstellen und während des Durchschleifens hinzufügen. xsSie 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 diese xsan eine Funktion übergeben, tailweil 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.)

Jimmy Hoffa
quelle
So wie ich es verstehe, aus Ihrer Antwort ist, dass in reiner funktionaler Programmierung, das gesamte Programm besteht aus vielen Einzel Ausdruck Funktionen aus, während in imperativen Programmierfunktionen (Verfahren) enthalten mehr als einen Ausdruck mit einer definierten Abfolge des Betriebes
ALXGTV
1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) Es handelt sich tatsächlich um mehrere Anweisungen / Ausdrücke. Dies hängt natürlich davon ab, was Sie als einen einzelnen Ausdruck betrachten. Um es zusammenzufassen: Funktionale Programmierung bedeutet im Grunde, ohne Semikolon zu programmieren.
ALXGTV
1
@ALXGTV Der Unterschied zwischen dieser Deklaration und einem imperativen Stil besteht darin, dass, wenn dieser mathematische Ausdruck Anweisungen wäre, sie unbedingt so ausgeführt werden müssten, wie sie geschrieben sind. Jedoch , weil es nicht eine Folge von Imperativ Aussagen ist , sondern vielmehr ein Ausdruck , der definiert , ein , was Sie wollen, weil es nicht sagen , wie es nicht zwingend notwendig ist , dass es wie geschrieben ausgeführt werden. Daher kann das Kompilieren die Ausdrücke reduzieren oder sogar als Literal speichern. Imperative Sprachen tun dies auch, aber nur, wenn Sie es in einer einzigen Anweisung angeben. eine Erklärung.
Jimmy Hoffa
@ALXGTV Deklarationen definieren Beziehungen, Beziehungen sind formbar; wenn x = 1 + 2dann x = 3und x = 4 - 1. In sequenzierten Anweisungen werden Anweisungen definiert. Wenn x = (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.
Jimmy Hoffa
Du hast einen fairen Punkt.
ALXGTV
6

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 + cals Grundlage und betrachten die Anweisung in einigen verschiedenen Sprachen, um die Idee zu erhalten:

Wenn wir a = b + cin einer imperativen Sprache wie C schreiben , weisen wir der Variablen den aktuellen Wert von b + czu aund nichts weiter. Wir machen keine grundsätzlichen Aussagen darüber, was aist. Vielmehr führen wir einfach einen Schritt in einem Prozess aus.

Wenn wir schreiben a = b + cin einer deklarativen Sprache, wie Microsoft Excel, (ja, Excel ist eine Programmiersprache und wahrscheinlich die deklarative von allen,) sind wir eine Beziehung zu behaupten zwischen a, bund cso , dass es immer , dass der Fall aist 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 + cauch eine unveränderliche Beziehung behauptet, aber nur weil bund cunverä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.

Daniel T.
quelle
0

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

f a b = let y = sum [a..b] in y*y

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!

Eine reine deklarative Sprache wäre für mich eine, die ausschließlich aus Deklarationen besteht

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)?

Ingo
quelle
Ich habe nicht angegeben, dass die funktionale Programmierung nicht anders ist als die Programmierung in C. Tatsächlich habe ich gesagt, dass Sprachen wie Haskell ein VIEL höheres Niveau haben als C und eine viel größere Abstraktion bieten.
ALXGTV