Bitte erläutern Sie einige Punkte von Paul Graham zu Lisp

146

Ich brauche Hilfe, um einige der Punkte aus Paul Grahams What Made Lisp Different zu verstehen .

  1. Ein neues Konzept von Variablen. In Lisp sind alle Variablen effektiv Zeiger. Werte haben Typen, keine Variablen, und das Zuweisen oder Binden von Variablen bedeutet das Kopieren von Zeigern, nicht das, worauf sie zeigen.

  2. Ein Symboltyp. Symbole unterscheiden sich von Zeichenfolgen darin, dass Sie die Gleichheit testen können, indem Sie einen Zeiger vergleichen.

  3. Eine Notation für Code unter Verwendung von Symbolbäumen.

  4. Die ganze Sprache ist immer verfügbar. Es gibt keinen wirklichen Unterschied zwischen Lesezeit, Kompilierungszeit und Laufzeit. Sie können Code beim Lesen kompilieren oder ausführen, Code beim Kompilieren lesen oder ausführen und zur Laufzeit Code lesen oder kompilieren.

Was bedeuten diese Punkte? Wie unterscheiden sie sich in Sprachen wie C oder Java? Haben andere Sprachen als die Sprachen der Lisp-Familie jetzt eines dieser Konstrukte?

unj2
quelle
10
Ich bin mir nicht sicher, ob das Tag für die funktionale Programmierung hier gerechtfertigt ist, da es in vielen Lisps genauso gut möglich ist, imperativen oder OO-Code zu schreiben wie funktionalen Code - und tatsächlich gibt es viele nicht funktionale Lisp Code herum. Ich würde vorschlagen, dass Sie das fp-Tag entfernen und stattdessen clojure hinzufügen - hoffentlich bringt dies einige interessante Eingaben von JVM-basierten Lispers.
Michał Marczyk
58
Wir haben paul-grahamhier auch einen Tag? !!! Großartig ...
fehlender Faktor
@missingfaktor Vielleicht braucht es eine Burninate-Anfrage
Katze

Antworten:

98

Matts Erklärung ist vollkommen in Ordnung - und er versucht es mit einem Vergleich mit C und Java, was ich nicht tun werde - aber aus irgendeinem Grund genieße ich es wirklich, ab und zu genau dieses Thema zu diskutieren. Hier ist meine Aufnahme bei einer Antwort.

Zu den Punkten (3) und (4):

Die Punkte (3) und (4) auf Ihrer Liste scheinen die interessantesten und derzeit noch relevantesten zu sein.

Um sie zu verstehen, ist es hilfreich, ein klares Bild davon zu haben, was mit Lisp-Code geschieht - in Form eines vom Programmierer eingegebenen Zeichenstroms - auf dem Weg zur Ausführung. Verwenden wir ein konkretes Beispiel:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

Dieser Ausschnitt des Clojure- Codes wird ausgedruckt aFOObFOOcFOO. Beachten Sie, dass Clojure den vierten Punkt auf Ihrer Liste möglicherweise nicht vollständig erfüllt, da die Lesezeit für Benutzercode nicht wirklich offen ist. Ich werde jedoch diskutieren, was es bedeuten würde, wenn dies anders wäre.

Nehmen wir also an, wir haben diesen Code irgendwo in einer Datei und bitten Clojure, ihn auszuführen. Nehmen wir außerdem an (der Einfachheit halber), dass wir es nach dem Bibliotheksimport geschafft haben. Das interessante Stück beginnt (printlnund endet )ganz rechts. Dies wird wie erwartet lexiert / analysiert, aber es ergibt sich bereits ein wichtiger Punkt: Das Ergebnis ist keine spezielle compilerspezifische AST-Darstellung - es handelt sich lediglich um eine reguläre Clojure / Lisp-Datenstruktur , nämlich eine verschachtelte Liste mit einer Reihe von Symbolen. Zeichenfolgen und - in diesem Fall - ein einzelnes kompiliertes Regex-Musterobjekt, das dem entspricht#"\d+"wörtlich (mehr dazu weiter unten). Einige Lisps fügen diesem Prozess ihre eigenen kleinen Wendungen hinzu, aber Paul Graham bezog sich hauptsächlich auf Common Lisp. In den für Ihre Frage relevanten Punkten ähnelt Clojure CL.

Die ganze Sprache zur Kompilierungszeit:

Nach diesem Punkt befasst sich der Compiler nur noch mit Lisp-Datenstrukturen (an die Lisp-Programmierer gewöhnt sind) (dies gilt auch für einen Lisp-Interpreter; Clojure-Code wird immer kompiliert). An dieser Stelle wird eine wunderbare Möglichkeit offensichtlich: Warum nicht Lisp-Programmierern erlauben, Lisp-Funktionen zu schreiben, die Lisp-Daten manipulieren, die Lisp-Programme darstellen, und transformierte Daten ausgeben, die transformierte Programme darstellen, die anstelle der Originale verwendet werden sollen? Mit anderen Worten - warum nicht Lisp-Programmierern erlauben, ihre Funktionen als Compiler-Plugins zu registrieren, die in Lisp als Makros bezeichnet werden? Und tatsächlich hat jedes anständige Lisp-System diese Kapazität.

Makros sind also reguläre Lisp-Funktionen, die zur Kompilierungszeit vor der letzten Kompilierungsphase, in der der eigentliche Objektcode ausgegeben wird, mit der Darstellung des Programms arbeiten. Da die Art der Codemakros, die ausgeführt werden dürfen, unbegrenzt ist (insbesondere wird der von ihnen ausgeführte Code häufig selbst unter großzügiger Verwendung der Makrofunktion geschrieben), kann man sagen, dass "die gesamte Sprache zur Kompilierungszeit verfügbar ist ".

Die ganze Sprache zur Lesezeit:

#"\d+"Kehren wir zu diesem Regex-Literal zurück. Wie oben erwähnt, wird dies zum Zeitpunkt des Lesens in ein tatsächlich kompiliertes Musterobjekt umgewandelt, bevor der Compiler die erste Erwähnung von neuem Code hört, der für die Kompilierung vorbereitet wird. Wie kommt es dazu?

Nun, die Art und Weise, wie Clojure derzeit implementiert ist, ist etwas anders als das, was Paul Graham im Sinn hatte, obwohl mit einem cleveren Hack alles möglich ist . In Common Lisp wäre die Geschichte konzeptionell etwas sauberer. Die Grundlagen sind jedoch ähnlich: Der Lisp Reader ist eine Zustandsmaschine, die nicht nur Zustandsübergänge durchführt und schließlich erklärt, ob sie einen "akzeptierenden Zustand" erreicht hat, sondern auch Lisp-Datenstrukturen ausspuckt, die die Zeichen darstellen. So werden die Zeichen 123zur Zahl 123usw. Der wichtige Punkt kommt jetzt: Diese Zustandsmaschine kann durch Benutzercode geändert werden. (Wie bereits erwähnt, ist dies in CLs Fall völlig richtig. Für Clojure ist ein Hack erforderlich (entmutigt und in der Praxis nicht verwendet). Aber ich schweife ab, es ist der Artikel von PG, auf den ich näher eingehen soll, also ...)

Wenn Sie also ein Common Lisp-Programmierer sind und die Idee von Vektorliteralen im Clojure-Stil mögen, können Sie einfach eine Funktion in den Reader einstecken, um angemessen auf eine Zeichenfolge zu reagieren - [oder #[möglicherweise - und sie als zu behandeln der Beginn eines Vektorliteral, der beim Abgleich endet ]. Eine solche Funktion wird als Lesemakro bezeichnet und kann wie ein normales Makro jede Art von Lisp-Code ausführen, einschließlich Code, der selbst mit funky Notation geschrieben wurde, die durch zuvor registrierte Lesermakros aktiviert wurde. Es gibt also die gesamte Sprache zum Zeitpunkt des Lesens für Sie.

Verpacken:

Tatsächlich wurde bisher gezeigt, dass man reguläre Lisp-Funktionen zur Lese- oder Kompilierungszeit ausführen kann; Der einzige Schritt, den man von hier aus unternehmen muss, um zu verstehen, wie Lesen und Kompilieren selbst zum Lesen, Kompilieren oder Ausführen möglich sind, besteht darin, zu erkennen, dass Lesen und Kompilieren selbst von Lisp-Funktionen ausgeführt werden. Sie können einfach Lisp-Daten aus Zeichenströmen aufrufen readoder evaljederzeit einlesen oder Lisp-Code kompilieren bzw. ausführen. Das ist die ganze Sprache genau dort, die ganze Zeit.

Beachten Sie, dass die Tatsache, dass Lisp Punkt (3) aus Ihrer Liste erfüllt, für die Art und Weise, wie es Punkt (4) erfüllt, wesentlich ist. Die besondere Art der von Lisp bereitgestellten Makros hängt stark davon ab, dass Code durch reguläre Lisp-Daten dargestellt wird. Das ist etwas, was durch (3) ermöglicht wird. Übrigens ist hier nur der "baumartige" Aspekt des Codes wirklich entscheidend - Sie könnten möglicherweise ein Lisp mit XML schreiben lassen.

Michał Marczyk
quelle
4
Achtung
Ken
Ken: Guter Fang, danke! Ich werde das in "normales Makro" ändern, was meiner Meinung nach wahrscheinlich niemanden stolpern lässt.
Michał Marczyk
Fantastische Antwort. Ich habe in 5 Minuten mehr daraus gelernt als in Stunden, in denen ich über die Frage gegoogelt / nachgedacht habe. Vielen Dank.
Charlie Flowers
Edit: argh, habe einen Folgesatz missverstanden. Grammatik korrigiert (brauche einen "Peer", um meine Bearbeitung zu akzeptieren).
Tatiana Racheva
S-Ausdrücke und XML können die gleichen Strukturen diktieren, aber XML ist weitaus ausführlicher und daher nicht als Syntax geeignet.
Sylwester
66

1) Ein neues Konzept von Variablen. In Lisp sind alle Variablen effektiv Zeiger. Werte haben Typen, keine Variablen, und das Zuweisen oder Binden von Variablen bedeutet das Kopieren von Zeigern, nicht das, worauf sie zeigen.

(defun print-twice (it)
  (print it)
  (print it))

'es' ist eine Variable. Es kann an JEDEN Wert gebunden werden. Der Variablen ist keine Einschränkung und kein Typ zugeordnet. Wenn Sie die Funktion aufrufen, muss das Argument nicht kopiert werden. Die Variable ähnelt einem Zeiger. Es gibt eine Möglichkeit, auf den Wert zuzugreifen, der an die Variable gebunden ist. Es ist nicht erforderlich, Speicher zu reservieren . Wir können jedes Datenobjekt übergeben, wenn wir die Funktion aufrufen: jede Größe und jeden Typ.

Die Datenobjekte haben einen 'Typ' und alle Datenobjekte können nach ihrem 'Typ' abgefragt werden.

(type-of "abc")  -> STRING

2) Ein Symboltyp. Symbole unterscheiden sich von Zeichenfolgen darin, dass Sie die Gleichheit testen können, indem Sie einen Zeiger vergleichen.

Ein Symbol ist ein Datenobjekt mit einem Namen. Normalerweise kann der Name verwendet werden, um das Objekt zu finden:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Da Symbole reale Datenobjekte sind, können wir testen, ob sie dasselbe Objekt sind:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

So können wir beispielsweise einen Satz mit Symbolen schreiben:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Jetzt können wir die Anzahl der THE im Satz zählen:

(count 'the *sentence*) ->  2

In Common Lisp haben Symbole nicht nur einen Namen, sondern können auch einen Wert, eine Funktion, eine Eigenschaftsliste und ein Paket haben. So können Symbole verwendet werden, um Variablen oder Funktionen zu benennen. Die Eigenschaftsliste wird normalerweise verwendet, um Symbolen Metadaten hinzuzufügen.

3) Eine Notation für Code unter Verwendung von Symbolbäumen.

Lisp verwendet seine grundlegenden Datenstrukturen zur Darstellung von Code.

Die Liste (* 3 2) kann sowohl Daten als auch Code sein:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

Der Baum:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Die ganze Sprache ist immer verfügbar. Es gibt keinen wirklichen Unterschied zwischen Lesezeit, Kompilierungszeit und Laufzeit. Sie können Code beim Lesen kompilieren oder ausführen, Code beim Kompilieren lesen oder ausführen und zur Laufzeit Code lesen oder kompilieren.

Lisp bietet die Funktionen READ zum Lesen von Daten und Code aus Text, LOAD zum Laden von Code, EVAL zum Auswerten von Code, COMPILE zum Kompilieren von Code und PRINT zum Schreiben von Daten und Code in Text.

Diese Funktionen sind immer verfügbar. Sie gehen nicht weg. Sie können Teil eines jeden Programms sein. Das heißt, jedes Programm kann Code lesen, laden, auswerten oder drucken - immer.

Wie unterscheiden sie sich in Sprachen wie C oder Java?

Diese Sprachen bieten keine Symbole, keinen Code als Daten oder keine Laufzeitauswertung von Daten als Code. Datenobjekte in C sind normalerweise untypisiert.

Haben andere Sprachen als die Sprachen der LISP-Familie jetzt eines dieser Konstrukte?

Viele Sprachen verfügen über einige dieser Funktionen.

Der Unterschied:

In Lisp sind diese Funktionen so in die Sprache integriert, dass sie einfach zu verwenden sind.

Rainer Joswig
quelle
33

Für die Punkte (1) und (2) spricht er historisch. Die Variablen von Java sind ziemlich gleich, weshalb Sie .equals () aufrufen müssen, um Werte zu vergleichen.

(3) spricht von S-Ausdrücken. Lisp-Programme sind in dieser Syntax geschrieben, die gegenüber Ad-hoc-Syntax wie Java und C viele Vorteile bietet, z. B. das Erfassen wiederholter Muster in Makros auf eine weitaus sauberere Weise als bei C-Makros oder C ++ - Vorlagen und das Bearbeiten von Code mit derselben Kernliste Operationen, die Sie für Daten verwenden.

(4) Nehmen wir zum Beispiel C: Die Sprache ist wirklich zwei verschiedene Subsprachen: Dinge wie if () und while () und der Präprozessor. Sie verwenden den Präprozessor, um zu sparen, dass Sie sich ständig wiederholen müssen, oder um Code mit # if / # ifdef zu überspringen. Beide Sprachen sind jedoch sehr unterschiedlich, und Sie können while () zur Kompilierungszeit nicht wie #if verwenden.

C ++ macht dies mit Vorlagen noch schlimmer. Schauen Sie sich einige Referenzen zur Vorlagen-Metaprogrammierung an, die eine Möglichkeit zum Generieren von Code zur Kompilierungszeit bietet und für Nicht-Experten äußerst schwierig ist, ihre Köpfe herumzureißen. Darüber hinaus gibt es eine Reihe von Hacks und Tricks mit Vorlagen und Makros, für die der Compiler keine erstklassige Unterstützung bieten kann. Wenn Sie einen einfachen Syntaxfehler machen, kann der Compiler Ihnen keine eindeutige Fehlermeldung geben.

Mit Lisp haben Sie das alles in einer einzigen Sprache. Sie verwenden das gleiche Material, um zur Laufzeit Code zu generieren, den Sie am ersten Tag gelernt haben. Dies soll nicht bedeuten, dass die Metaprogrammierung trivial ist, aber mit erstklassiger Sprach- und Compilerunterstützung ist sie sicherlich einfacher.

Matt Curtis
quelle
7
Außerdem ist diese Kraft (und Einfachheit) mittlerweile über 50 Jahre alt und so einfach zu implementieren, dass ein unerfahrener Programmierer sie mit minimaler Anleitung ausprobieren und die Grundlagen der Sprache erlernen kann. Sie würden keine ähnliche Behauptung von Java, C, Python, Perl, Haskell usw. als ein gutes Anfängerprojekt hören!
Matt Curtis
9
Ich denke nicht, dass Java-Variablen überhaupt wie Lisp-Symbole sind. In Java gibt es keine Notation für ein Symbol, und das einzige, was Sie mit einer Variablen tun können, ist, ihre Wertzelle abzurufen. Strings können interniert werden, aber es handelt sich normalerweise nicht um Namen. Daher ist es nicht einmal sinnvoll, darüber zu sprechen, ob sie zitiert, bewertet, bestanden usw. werden können
Ken
2
Über 40 Jahre alt könnte genauer sein :), @Ken: Ich denke, er meint, dass 1) nicht-primitive Variablen in Java durch Referenz sind, was lisp ähnelt, und 2) internierte Zeichenfolgen in Java Symbolen in lisp ähnlich sind - Natürlich können Sie, wie Sie sagten, internierte Zeichenfolgen / Code in Java nicht zitieren oder bewerten, daher sind sie immer noch sehr unterschiedlich.
3
@ Dan - Ich bin mir nicht sicher, wann die erste Implementierung zusammengestellt wurde, aber das erste McCarthy-Papier über symbolische Berechnungen wurde 1960 veröffentlicht.
Inaimathi
Java unterstützt teilweise / unregelmäßig „Symbole“ in Form von Foo.class / foo.getClass () - dh ein Typ <Foo> -Objekt vom Typ eines Typs ist etwas analog - ebenso wie Enum-Werte zu ein Grad. Aber sehr minimale Schatten eines Lisp-Symbols.
BRPocock
-3

Die Punkte (1) und (2) würden auch zu Python passen. Anhand eines einfachen Beispiels "a = str (82.4)" erstellt der Interpreter zunächst ein Gleitkommaobjekt mit dem Wert 82.4. Dann ruft es einen String-Konstruktor auf, der dann einen String mit dem Wert '82 .4 'zurückgibt. Das 'a' auf der linken Seite ist lediglich eine Bezeichnung für dieses Zeichenfolgenobjekt. Das ursprüngliche Gleitkommaobjekt wurde durch Müll gesammelt, da keine Verweise mehr darauf vorhanden sind.

In Schema wird alles auf ähnliche Weise als Objekt behandelt. Bei Common Lisp bin ich mir nicht sicher. Ich würde versuchen, nicht in C / C ++ - Konzepten zu denken. Sie haben mich langsamer gemacht, als ich versuchte, mich mit der schönen Einfachheit von Lisps vertraut zu machen.

CyberFonic
quelle