it-swarm.com.de

Der Traum von deklarativer Programmierung

Warum wurde der Traum von deklarativer Programmierung nicht verwirklicht? Welche konkreten Hindernisse stehen im Weg? Für ein einfaches Beispiel, warum kann ich nicht sagen

sort(A) is defined by sort(A) in perm(A) && asc(sort(A))

und automatisch einen Sortieralgorithmus daraus erhalten. perm bedeutet Permutationen und asc bedeutet aufsteigend.

26
davidk01

Es gibt einige sehr gute Antworten. Ich werde versuchen, zur Diskussion beizutragen.

Zum Thema deklarative, logische Programmierung in Prolog gibt es das großartige Buch "The Craft of Prolog" von Richard O'Keefe . Es geht darum, effiziente Programme mit einer Programmiersprache zu schreiben, mit der Sie sehr ineffiziente Programme schreiben können. In diesem Buch geht der Autor bei der Erörterung der effizienten Implementierung mehrerer Algorithmen (im Kapitel "Methoden der Programmierung") folgendermaßen vor:

  • definieren Sie das Problem auf Englisch
  • schreiben Sie eine funktionierende Lösung, die so aussagekräftig wie möglich ist. Normalerweise bedeutet das ziemlich genau das, was Sie in Ihrer Frage haben, nur den richtigen Prolog
  • führen Sie von dort aus Schritte aus, um die Implementierung zu verfeinern und schneller zu machen

Die aufschlussreichste (für mich) Beobachtung, die ich machen konnte, während ich mich durch diese arbeitete:

Ja, die endgültige Version der Implementierung ist viel effizienter als die "deklarative" Spezifikation, mit der der Autor begonnen hat. Es ist immer noch sehr aussagekräftig, prägnant und leicht zu verstehen. Was dazwischen passiert ist, ist, dass die endgültige Lösung die Eigenschaften des Problems erfasst, für das die ursprüngliche Lösung keine Ahnung hatte.

Mit anderen Worten, bei der Implementierung einer Lösung haben wir so viel Wissen wie möglich über das Problem genutzt. Vergleichen Sie:

Suchen Sie eine Permutation einer Liste, sodass alle Elemente in aufsteigender Reihenfolge sind

zu:

Das Zusammenführen von zwei sortierten Listen führt zu einer sortierten Liste. Da es möglicherweise bereits sortierte Unterlisten gibt, verwenden Sie diese als Ausgangspunkt anstelle von Unterlisten der Länge 1.

Eine kleine Seite: Eine Definition wie die, die Sie gegeben haben, ist attraktiv, weil sie sehr allgemein ist. Ich kann mich jedoch nicht dem Gefühl entziehen, dass es die Tatsache, dass Permutationen ein kombinatorisches Problem sind, absichtlich ignoriert. Dies ist etwas, was wir bereits wissen! Dies ist keine Kritik, nur eine Beobachtung.

Zur eigentlichen Frage: Wie geht es weiter? Eine Möglichkeit besteht darin, möglichst viel Wissen über das Problem bereitzustellen, das wir dem Computer melden.

Der beste mir bekannte Versuch, das Problem wirklich zu lösen, wird in den von Alexander Stepanov mitverfassten Büchern vorgestellt: "Elemente der Programmierung" und "Von der Mathematik zur generischen Programmierung" . Ich bin leider nicht in der Lage, alles in diesen Büchern zusammenzufassen (oder sogar vollständig zu verstehen). Der dortige Ansatz besteht jedoch darin, effiziente (oder sogar optimale) Bibliotheksalgorithmen und Datenstrukturen zu definieren, vorausgesetzt, dass alle relevanten Eigenschaften der Eingabe im Voraus bekannt sind. Das Endergebnis ist:

  • Jede genau definierte Transformation ist eine Verfeinerung der bereits vorhandenen Einschränkungen (der bekannten Eigenschaften).
  • Wir lassen den Computer anhand der vorhandenen Einschränkungen entscheiden, welche Transformation optimal ist.

Warum es noch nicht ganz passiert ist, nun, die Informatik ist ein wirklich junges Gebiet, und wir kommen immer noch damit zurecht, die Neuheit des größten Teils davon wirklich zu schätzen.

[~ # ~] ps [~ # ~]

Um Ihnen einen Vorgeschmack auf das zu geben, was ich unter "Verfeinern der Implementierung" verstehe: Nehmen Sie zum Beispiel das einfache Problem, das letzte Element einer Liste in Prolog zu erhalten. Die kanonische deklarative Lösung lautet:

last(List, Last) :-
    append(_, [Last], List).

Hier ist die deklarative Bedeutung von append/3 ist:

List1AndList2 ist die Verkettung von List1 und List2

Da im zweiten Argument zu append/3 wir haben eine Liste mit nur einem Element, und das erste Argument wird ignoriert (der Unterstrich), wir erhalten eine Aufteilung der ursprünglichen Liste, die den Anfang der Liste verwirft (List1 im Zusammenhang mit append/3) und verlangt, dass der Rücken (List2 im Zusammenhang mit append/3) ist in der Tat eine Liste mit nur einem Element: es ist also das letzte Element.

Das tatsächliche Implementierung von SWI-Prolog sagt jedoch:

last([X|Xs], Last) :-
    last_(Xs, X, Last).

last_([], Last, Last).
last_([X|Xs], _, Last) :-
    last_(Xs, X, Last).

Dies ist immer noch schön deklarativ. Lesen Sie von oben nach unten, es heißt:

Das letzte Element einer Liste ist nur für eine Liste mit mindestens einem Element sinnvoll. Das letzte Element für ein Paar aus dem Schwanz und dem Kopf einer Liste ist also: der Kopf, wenn der Schwanz leer ist, oder der letzte des nicht leeren Schwanzes.

Der Grund, warum diese Implementierung bereitgestellt wird, besteht darin, die praktischen Probleme rund um das Ausführungsmodell von Prolog zu umgehen. Im Idealfall sollte es keinen Unterschied machen, welche Implementierung verwendet wird. Ebenso hätten wir sagen können:

last(List, Last) :-
    reverse(List, [Last|_]).

Das letzte Element einer Liste ist das erste Element der umgekehrten Liste.

Wenn Sie sich mit nicht schlüssigen Diskussionen über das gute, deklarative Prolog satt fühlen möchten, gehen Sie einfach einige der Fragen und Antworten im Prolog-Tag auf Stapelüberlauf durch.

8
XXX

Logische Sprachen tun dies bereits. Sie können die Sortierung ähnlich definieren, wie Sie es tun.

Das Hauptproblem ist die Leistung. Computer sind zwar gut darin, viele Dinge zu berechnen, aber sie sind von Natur aus dumm. Jede "kluge" Entscheidung, die ein Computer treffen könnte, wurde von einem Programmierer programmiert. Und diese Entscheidung wird normalerweise nicht dadurch beschrieben, wie das Endergebnis aussieht, sondern wie dieses Endergebnis Schritt für Schritt erreicht wird.

Stellen Sie sich die Geschichte eines Golem vor. Wenn Sie versuchen, ihm einen abstrakten Befehl zu geben, wird er dies bestenfalls ineffizient tun und im schlimmsten Fall sich selbst, Sie oder jemand anderen verletzen. Wenn Sie jedoch genau beschreiben, was Sie möchten, können Sie sicher sein, dass die Aufgabe effektiv und effizient erledigt wird.

Es ist Aufgabe des Programmierers, zu entscheiden, welche Abstraktionsebene verwendet werden soll. Gehen Sie für die Anwendung, die Sie erstellen, auf hoher Ebene und beschreiben Sie sie abstrakt und nehmen Sie den Leistungseinbruch oder gehen Sie niedrig und schmutzig, verbringen Sie 10x mehr Zeit damit, aber erhalten Sie einen Algorithmus, der 1000x leistungsfähiger ist?

50
Euphoric

Zusätzlich zu Euphorics hervorragender Punkt möchte ich hinzufügen, dass wir bereits deklarative Sprachen an vielen Stellen verwenden, an denen sie gut funktionieren, dh einen Zustand beschreiben, der sich wahrscheinlich nicht ändert, oder etwas anfordern, für das Der Computer kann tatsächlich selbst effizienten Code generieren:

  • HTML deklariert den Inhalt einer Webseite.

  • CSS legt fest, wie verschiedene Arten von Elementen auf einer Webseite aussehen sollen.

  • Jede relationale Datenbank verfügt über eine Datendefinitionssprache, die die Struktur der Datenbank angibt.

  • SQL ist deklarativ viel näher als zwingend, da Sie ihm mitteilen, was Sie sehen möchten, und der Abfrageplaner der Datenbank herausfindet, wie dies tatsächlich geschehen kann.

  • Man könnte argumentieren, dass die meisten Konfigurationsdateien (.vimrc, .profile, .bashrc, .gitconfig) eine domänenspezifische Sprache verwenden, die weitgehend deklarativ ist

45
Ixrec

Abstraktionen sind undicht

Sie können ein deklaratives System implementieren, in dem Sie deklarieren, was Sie möchten, und der Compiler oder Interpretator eine Ausführungsreihenfolge ermittelt. Der theoretische Vorteil besteht darin, dass Sie nicht mehr über das Wie nachdenken müssen und diese Implementierung nicht detailliert beschreiben müssen. In der Praxis für das Allzweck-Computing müssen Sie jedoch immer noch über das Wie nachdenken und alle Arten von Tricks schreiben, wobei Sie berücksichtigen müssen, wie dies implementiert wird, da der Compiler andernfalls eine Implementierung auswählen kann (und dies häufig tun wird) sehr, sehr, sehr langsam (zB n! Operationen, bei denen n ausreichen würde).

In Ihrem speziellen Beispiel erhalten Sie [~ # ~] einen [~ # ~] Sortieralgorithmus - dies bedeutet nicht, dass Sie einen erhalten gut oder sogar etwas brauchbar. Ihre angegebene Definition führt, wenn sie wörtlich implementiert wird (wie es ein Compiler wahrscheinlich tun würde), zu http://en.wikipedia.org/wiki/Bogosort , was für größere Datensätze unbrauchbar ist - technisch korrekt, aber erforderlich eine Ewigkeit, um tausend Zahlen zu sortieren.

Für einige begrenzte Domänen können Sie Systeme schreiben, die fast immer gut darin sind, eine gute Implementierung zu finden, z. B. SQL. Für Allzweck-Computing, das nicht besonders gut funktioniert - Sie können Systeme beispielsweise in Prolog schreiben, aber Sie müssen sich vorstellen, wie genau Ihre Deklarationen am Ende in eine zwingende Ausführungsreihenfolge konvertiert werden, und das verliert viel von der erwarteten Deklaration Programmiervorteile.

17
Peteris

Die rechnergestützte Entscheidbarkeit ist der wichtigste Grund, warum sich die deklarative Programmierung als nicht so einfach erwiesen hat, wie es scheint.

Viele Probleme, die relativ einfach zu benennen sind, haben sich als unentscheidbar erwiesen oder haben eine NP-vollständige Komplexität zu lösen. Dies tritt häufig auf, wenn wir negative Klassen und Klassifizierung, Zählbarkeit und Rekursion berücksichtigen.

Ich möchte dies anhand einiger bekannter Domänen veranschaulichen.

Die Entscheidung, welche CSS-Klasse verwendet werden soll, erfordert Kenntnisse und die Berücksichtigung aller CSS-Regeln. Das Hinzufügen neuer Regeln kann alle anderen Entscheidungen ungültig machen. Negative CSS-Klassen werden absichtlich aufgrund von NP-vollständigen Problemen nicht zur Sprache hinzugefügt, aber das Fehlen negativer Klassen erschwert CSS-Entwurfsentscheidungen.

Innerhalb eines (SQL-) Abfrageoptimierers besteht das lästige Problem, zu entscheiden, in welcher Reihenfolge verbunden werden soll, welche Indizes verwendet werden sollen und welcher Speicher welchen temporären Ergebnissen zugeordnet werden soll. Dies ist ein bekanntes NP-vollständiges Problem und erschwert das Datenbankdesign und die Abfrageformulierung. Um es anders zu formulieren: Beim Entwerfen einer Datenbank oder einer Abfrage muss der Designer die Aktionen und die Reihenfolge der Aktionen kennen, die der Abfrageoptimierer wahrscheinlich ausführen wird. Ein erfahrener Ingenieur benötigt Kenntnisse über die Heuristiken, die von großen Datenbankanbietern verwendet werden.

Konfigurationsdateien sind deklarativ, bestimmte Konfigurationen sind jedoch schwer zu deklarieren. Um beispielsweise Funktionen richtig zu konfigurieren, müssen Versionierung, Bereitstellung (und Bereitstellungsverlauf), mögliche manuelle Überschreibungen und mögliche Konflikte mit anderen Einstellungen berücksichtigt werden. Das ordnungsgemäße Überprüfen einer Konfiguration kann zu einem NP-vollständigen Problem werden.

Das Ergebnis ist, dass diese Komplikationen Anfänger überraschen, die „Schönheit“ der deklarativen Programmierung zerstören und einige Ingenieure dazu veranlassen, nach anderen Lösungen zu suchen. Die Migration unerfahrener Ingenieure von SQL zu NoSQL wurde möglicherweise durch die zugrunde liegende Komplexität relationaler Datenbanken ausgelöst.

11
Dibbeke

Wir haben einen Unterschied in der Deklarativität von Programmiersprachen, der bei der Überprüfung der digitalen Logik gut genutzt wird.

Normalerweise wird digitale Logik bei Registerübertragungspegel (RTL) beschrieben, wo der Logikpegel der Signale zwischen Registern definiert ist. Um zu überprüfen, fügen wir zunehmend Eigenschaften hinzu, die abstrakter und deklarativer definiert sind.

Eine der deklarativeren Sprachen/Sprachuntergruppen heißt PSL für die Eigenschaftsspezifikationssprache. Beim Testen eines RTL-Modells eines Multiplikators , in dem beispielsweise alle logischen Operationen des Verschiebens und Addierens über mehrere Taktzyklen angegeben werden müssen; Sie können eine Eigenschaft in der Art von assert that when enable is high, this output will equal the multiplication of these two inputs after no more than 8 clock cycles schreiben. Die PSL-Beschreibung kann dann zusammen mit der RTL in einer Simulation überprüft werden, oder es kann formal nachgewiesen werden, dass die PSL für die RTL-Beschreibung gilt.

Das deklarativere PSL-Modell zwingt dazu, dasselbe Verhalten wie die RTL-Beschreibung zu beschreiben, jedoch auf eine ausreichend andere Weise, die automatisch mit der RTL verglichen werden kann, um festzustellen, ob sie übereinstimmen.

2
Paddy3118

Das Problem besteht hauptsächlich darin, wie Sie die Daten modellieren. und deklarative Programmierung hilft hier nicht. In imperativen Sprachen haben Sie bereits Tonnen von Bibliotheken, die viele Dinge für Sie erledigen, sodass Sie nur wissen müssen, wie Sie anrufen sollen. In besonderer Weise könnte man diese deklarative Programmierung betrachten (wahrscheinlich das beste Beispiel dafür ist Stream API in Java 8 ). Damit ist die Abstraktion bereits aufgelöst und eine deklarative Programmierung ist nicht erforderlich.

Wie bereits gesagt, machen logische Programmiersprachen bereits genau das, was Sie wollen. Man könnte sagen, das Problem ist die Leistung, aber mit der heutigen Hardware und Forschung in diesem Bereich können die Dinge verbessert werden, um für den produktiven Einsatz bereit zu sein. Eigentlich wird Prolog für KI-Sachen verwendet, aber ich glaube nur von der Wissenschaft.

Es ist zu beachten, dass dies für allgemeine Programmiersprachen gilt. Für domänenspezifische Sprachen sind deklarative Sprachen viel besser. SQL ist wahrscheinlich das beste Beispiel.

1
m3th0dman

Es würde ungefähr so ​​aussehen .. {(was auch immer => eine Datei lesen und eine URL aufrufen) | Rufen Sie eine URL auf und lesen Sie eine Datei.} Dies sind jedoch auszuführende Aktionen, und der Status des Systems ändert sich infolgedessen. Dies ist jedoch aus der Quelle nicht ersichtlich.

Deklarative können eine endliche Zustandsmaschine und ihre Übergänge beschreiben. Der FSM ist wie das Gegenteil von Deklarativen ohne Aktionen, auch wenn die einzige Aktion darin besteht, den Status in den nächsten Status zu ändern.

Der Vorteil dieser Methode besteht darin, dass Übergänge und Aktionen durch Prädikate angegeben werden können, die nicht nur für einen, sondern für mehrere Übergänge gelten.

Ich weiß, das klingt etwas seltsam, aber 2008 habe ich einen Programmgenerator geschrieben, der diese Methode verwendet, und das generierte C++ ist zwei- bis fünfzehnmal so viel wie die Quelle. Ich habe jetzt über 75.000 Zeilen C++ aus 20.000 Eingabezeilen. Dazu gehören zwei Dinge: Konsistenz und Vollständigkeit.

Konsistenz: Keine zwei Prädikate, die gleichzeitig wahr sein können, können inkonsistente Aktionen implizieren, da sowohl x = 8 als auch x = 9 oder unterschiedliche nächste Zustände.

Vollständigkeit: Die Logik für jeden Zustandsübergang wird angegeben. Diese können für Systeme mit N Unterzuständen mit> 2 ** N Zuständen schwierig zu überprüfen sein, aber es gibt interessante kombinatorische Methoden, die alles verifizieren können. 1962 schrieb ich Phase 1 einer Systemsortierung für die 7070-Maschinen, wobei ich diese Art der bedingten Codegenerierung und des kombinatorischen Debugs verwendete. Von den 8.000 Zeilen in der Sorte war die Anzahl der Fehler ab dem Tag der ersten Veröffentlichung für immer Null!

Die zweite Phase dieser Art, 12.000 Zeilen, hatte in den ersten zwei Monaten über 60 Fehler. Dazu gibt es noch viel mehr zu sagen, aber es funktioniert. Wenn Autohersteller diese Methode verwenden würden, um die Firmware zu überprüfen, würden wir die Fehler, die wir jetzt sehen, nicht sehen.

0
Luther Woodrum