90 Stimmen

LET versus LET* in Common Lisp

Ich verstehe den Unterschied zwischen LET und LET* (parallele und sequentielle Bindung), und theoretisch macht das auch durchaus Sinn. Aber gibt es einen Fall, in dem Sie LET jemals wirklich gebraucht haben? In all meinem Lisp-Code, den ich mir in letzter Zeit angesehen habe, könnte man jedes LET durch LET* ersetzen, ohne dass sich etwas ändert.

Edit: OK, ich verstehe warum Irgendjemand hat LET* erfunden, vermutlich als Makro, vor langer Zeit. Meine Frage ist, ob es angesichts der Existenz von LET* einen Grund gibt, LET beizubehalten? Haben Sie schon einmal Lisp-Code geschrieben, in dem ein LET* nicht so gut funktioniert wie ein einfaches LET?

Ich glaube nicht an das Argument der Effizienz. Erstens scheint es nicht so schwer zu sein, Fälle zu erkennen, in denen LET* in etwas so Effizientes wie LET kompiliert werden kann. Zweitens gibt es eine Menge Dinge in der CL-Spezifikation, die einfach nicht so aussehen, als wären sie auf Effizienz ausgelegt. (Wann haben Sie das letzte Mal eine LOOP mit Typdeklarationen gesehen? Die sind so schwer zu verstehen, dass ich sie noch nie benutzt gesehen habe.) Vor Dick Gabriels Benchmarks in den späten 1980er Jahren war CL war geradezu langsam.

Es sieht so aus, als sei dies ein weiterer Fall von Abwärtskompatibilität: wohlweislich wollte niemand riskieren, etwas so Grundlegendes wie LET zu zerstören. Das war meine Vermutung, aber es ist beruhigend zu hören, dass niemand einen dumm-einfachen Fall hat, den ich übersehen habe, in dem LET eine Reihe von Dingen lächerlich einfacher gemacht hat als LET*.

0 Stimmen

Parallel ist eine schlechte Wortwahl; nur vorherige Bindungen sind sichtbar. parallele Bindungen wären eher wie Haskells "... where ..." Bindungen.

1 Stimmen

Ich wollte nicht verwirren; ich glaube, das sind die Worte, die in der Spezifikation verwendet werden :-)

12 Stimmen

Die Parallele ist richtig. Das bedeutet, dass die Bindungen gleichzeitig zum Leben erwachen und sich nicht gegenseitig sehen und beschatten. Zu keinem Zeitpunkt gibt es eine für den Benutzer sichtbare Umgebung, die einige der in der LET definierten Variablen enthält, andere aber nicht.

93voto

Rainer Joswig Punkte 131198

LET selbst ist kein echtes Primitivum in einer Funktionale Programmiersprache ersetzt werden kann, da sie durch LAMBDA . Etwa so:

(let ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

ist vergleichbar mit

((lambda (a1 a2 ... an)
   (some-code a1 a2 ... an))
 b1 b2 ... bn)

Aber

(let* ((a1 b1) (a2 b2) ... (an bn))
  (some-code a1 a2 ... an))

ist vergleichbar mit

((lambda (a1)
    ((lambda (a2)
       ...
       ((lambda (an)
          (some-code a1 a2 ... an))
        bn))
      b2))
   b1)

Sie können sich vorstellen, was einfacher ist. LET und nicht LET* .

LET macht den Code leichter verständlich. Man sieht ein Bündel von Bindungen und kann jede Bindung einzeln lesen, ohne den Top-Down-/Links-Rechts-Fluss der "Effekte" (Rückbindungen) verstehen zu müssen. Verwendung von LET* signalisiert dem Programmierer (demjenigen, der den Code liest), dass die Bindungen nicht unabhängig sind, sondern dass es eine Art Top-Down-Fluss gibt - was die Sache verkompliziert.

Common Lisp hat die Regel, dass die Werte für die Bindungen in LET werden von links nach rechts berechnet. Genau wie die Werte für einen Funktionsaufruf ausgewertet werden - von links nach rechts. So, LET ist die konzeptionell einfachere Anweisung und sollte standardmäßig verwendet werden.

Typen in LOOP ? werden recht häufig verwendet. Es gibt einige primitive Formen der Typendeklaration, die leicht zu merken sind. Beispiel:

(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))

Oben wird die Variable i zu sein fixnum .

Richard P. Gabriel veröffentlichte 1985 sein Buch über Lisp-Benchmarks, und zu dieser Zeit wurden diese Benchmarks auch mit Nicht-CL-Lisp verwendet. Common Lisp selbst war 1985 brandneu - die CLtL1 Das Buch, in dem die Sprache beschrieben wird, war gerade 1984 erschienen. Kein Wunder, dass die Implementierungen zu dieser Zeit nicht sehr optimiert waren. Die implementierten Optimierungen waren im Grunde die gleichen (oder weniger), die die Implementierungen davor hatten (wie MacLisp).

Aber für LET vs. LET* Der Hauptunterschied besteht darin, dass Code mit LET ist für Menschen leichter zu verstehen, da die Bindungsklauseln unabhängig voneinander sind - zumal es schlechter Stil ist, die Auswertung von links nach rechts auszunutzen (ohne Variablen als Nebeneffekt zu setzen).

3 Stimmen

Nein, nein! Lambda ist kein echtes Primitivum, da es durch LET und ein Lambda auf niedrigerer Ebene ersetzt werden kann, das lediglich eine API für den Zugriff auf die Argumentwerte bietet: (low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...) :)

0 Stimmen

Diese Antwort trifft nicht zu, da die Lambda-Parameter keine initialisierenden Ausdrücke haben, die in der Umgebung ausgewertet werden. Das heißt also, dass (lambda (a b c) ...) ist nicht spezifisch äquivalent zu let ou let* in dieser Hinsicht. Die lambda Ausdruck erzeugt ein Laufzeitobjekt, und die Bindung der Parameter erfolgt später, wenn dieses Objekt aufgerufen wird. Die Ausdrücke, die die Werte erzeugen, befinden sich in einem völlig anderen Bereich, vielleicht in einer anderen kompilierten Datei. [ Fortsetzung ]

0 Stimmen

In Common Lisp und ähnlichen Dialekten gibt es eine Situation, in der Lambda-Parameter tun haben initialisierende Ausdrücke: (lambda (&optional (a x) (b y) ...)) . *Diese optionalen Parameter lauten `letsequentielle Bindung, und nichtletparallele Bindung.** . Zusammenfassend lässt sich also sagen, dass, wenn wir optionale Parameter mit Standardwertausdrücken inlambda` Die Frage nach Parallelität und Sequenzialität ist nur eine Frage der Implementierung mit Vor- und Nachteilen; keine der beiden Optionen ist von geringerer Bedeutung oder grundlegender als die andere.

40voto

Mr Fooz Punkte 102791

Sie haben keine brauchen LET, aber normalerweise wollen il.

LET deutet darauf hin, dass Sie nur eine Standard-Parallelbindung durchführen, ohne dass es irgendwelche Schwierigkeiten gibt. LET* führt zu Einschränkungen für den Compiler und suggeriert dem Benutzer, dass es einen Grund gibt, warum sequentielle Bindungen erforderlich sind. In Bezug auf Stil LET ist besser, wenn Sie die zusätzlichen Einschränkungen von LET* nicht brauchen.

Es kann effizienter sein, LET als LET* zu verwenden (je nach Compiler, Optimierer usw.):

  • parallele Bindungen können sein parallel ausgeführt (aber ich weiß nicht, ob irgendwelche LISP-Systeme dies tatsächlich tun, und die Init-Formulare müssen immer noch sequentiell ausgeführt werden)
  • Parallele Bindungen schaffen eine einzige neue Umgebung (Scope) für alle Bindungen. Sequentielle Bindungen erstellen eine neue verschachtelte Umgebung für jede einzelne Bindung. Parallele Bindungen verwenden weniger Speicherplatz und haben schnelleres Nachschlagen von Variablen .

(Die obigen Aufzählungspunkte gelten für Scheme, einen anderen LISP-Dialekt. clisp kann davon abweichen.)

2 Stimmen

Anmerkung: Siehe diese Antwort (und/oder den verlinkten Abschnitt von hyperspec), um zu erklären, warum Ihr erster Punkt mit den Junghennen - sagen wir mal - irreführend ist. Die Bindungen parallel ablaufen, aber die Formulare werden nacheinander ausgeführt - gemäß der Spezifikation.

6 Stimmen

Die parallele Ausführung wird vom Common-Lisp-Standard in keiner Weise berücksichtigt. Auch die schnellere Suche nach Variablen ist ein Mythos.

2 Stimmen

Der Unterschied ist nicht nur für den Compiler wichtig. Ich verwende let und let* als Hinweis für mich selbst, was vor sich geht. Wenn ich let in meinem Code sehe, weiß ich, dass die Bindungen unabhängig sind, und wenn ich let* sehe, weiß ich, dass die Bindungen voneinander abhängen. Aber ich weiß das nur, weil ich darauf achte, let und let* konsequent zu verwenden.

29voto

Logan Capaldo Punkte 38523

Ich komme mit erfundenen Beispielen. Vergleichen Sie das Ergebnis:

(print (let ((c 1))
         (let ((c 2)
               (a (+ c 1)))
           a)))

mit dem Ergebnis der Durchführung dieses Vorgangs:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))

4 Stimmen

Können Sie uns sagen, warum dies der Fall ist?

4 Stimmen

@John: im ersten Beispiel, a bezieht sich die Bindung auf den äußeren Wert von c . Im zweiten Beispiel, bei dem let* erlaubt es Bindungen, auf frühere Bindungen zu verweisen, a Die Bindung bezieht sich auf den inneren Wert von c . Logan lügt nicht, wenn er sagt, dass dies ein erfundenes Beispiel ist, und es gibt nicht einmal vor, nützlich zu sein. Auch die Einrückung ist nicht standardisiert und irreführend. In beiden, a sollte um ein Leerzeichen verschoben werden, um sich mit c und der "Körper" des inneren let sollte nur zwei Leerzeichen von der let selbst.

3 Stimmen

Diese Antwort liefert eine wichtige Erkenntnis. Man würde speziell verwenden. let wenn man es möchte vermeiden sekundäre Bindungen (d. h. nicht die erste) auf die erste Bindung verweisen, aber Sie tun eine frühere Bindung schattieren möchten, indem Sie den früheren Wert dieser Bindung für die Initialisierung einer Ihrer sekundären Bindungen verwenden.

11voto

David Thornley Punkte 55244

In LISP besteht oft der Wunsch, die schwächsten möglichen Konstrukte zu verwenden. Einige Styleguides empfehlen die Verwendung von = statt eql wenn Sie wissen, dass die verglichenen Elemente numerisch sind, zum Beispiel. Oft geht es eher darum, zu spezifizieren, was man meint, als den Computer effizient zu programmieren.

Es kann jedoch zu tatsächlichen Effizienzsteigerungen führen, wenn man nur sagt, was man meint, und keine stärkeren Konstrukte verwendet. Wenn Sie Initialisierungen mit LET können sie parallel ausgeführt werden, während LET* Initialisierungen müssen sequentiell ausgeführt werden. Ich weiß nicht, ob es Implementierungen gibt, die das tatsächlich tun, aber einige werden es in Zukunft vielleicht tun.

2 Stimmen

Gutes Argument. Da Lisp jedoch eine Hochsprache ist, frage ich mich, warum "möglichst schwache Konstrukte" in Lisp ein so beliebter Stil ist. Man sieht keine Perl-Programmierer, die sagen: "Nun, wir wollen nicht besoin um hier eine Regexp zu verwenden..." :-)

1 Stimmen

Ich weiß es nicht, aber es gibt eine eindeutige Stilpräferenz. Dagegen stehen Leute (wie ich), die so oft wie möglich die gleiche Form verwenden wollen (ich schreibe fast nie setq statt setf). Vielleicht hat es etwas mit der Idee zu tun, zu sagen, was man meint.

0 Stimmen

En = Operator weder stärker noch schwächer ist als eql . Es ist ein schwächerer Test, weil 0 ist gleich 0.0 . Sie ist aber auch deshalb stärker, weil nichtnumerische Argumente zurückgewiesen werden.

10voto

tmh Punkte 231

Der Hauptunterschied in Common List zwischen LET und LET* besteht darin, dass die Symbole in LET parallel und in LET* sequentiell gebunden werden. Bei der Verwendung von LET können die Init-Formulare nicht parallel ausgeführt werden, und die Reihenfolge der Init-Formulare kann nicht geändert werden. Der Grund dafür ist, dass Common Lisp Funktionen mit Seiteneffekten erlaubt. Daher ist die Reihenfolge der Auswertung wichtig und erfolgt innerhalb eines Formulars immer von links nach rechts. In LET werden also zuerst die Init-Formulare von links nach rechts ausgewertet, dann werden die Bindungen erstellt, von links nach rechts parallel zueinander. In LET* wird die Init-Form ausgewertet und dann der Reihe nach von links nach rechts an das Symbol gebunden.

CLHS: Spezialoperator LET, LET*

2 Stimmen

Es hat den Anschein, dass diese Antwort vielleicht etwas Energie daraus zieht, dass sie eine Antwort auf diese Antwort ? Außerdem ist laut der verlinkten Spezifikation die Bindungen werden parallel durchgeführt in LET auch wenn Sie zu Recht darauf hinweisen, dass die Init-Formulare nacheinander ausgeführt werden. Ob das in einer bestehenden Implementierung einen praktischen Unterschied macht, weiß ich nicht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X