it-swarm.com.de

LET versus LET * in Common Lisp

Ich verstehe den Unterschied zwischen LET und LET * (paralleles und sequentielles Binden), und als theoretische Angelegenheit macht es vollkommenen Sinn. Aber gibt es einen Fall, in dem Sie jemals LET benötigt haben? In meinem gesamten LISP-Code, den ich kürzlich angesehen habe, können Sie jedes LET ohne Änderungen durch LET * ersetzen.

Edit: OK, ich verstehe warum ein Typ hat LET * erfunden, vermutlich als Makro, als damals. Meine Frage ist, ob es LET * gibt. Gibt es einen Grund für LET, um zu bleiben? Haben Sie einen tatsächlichen LISP-Code geschrieben, bei dem ein LET * nicht so gut funktionieren würde wie ein einfaches LET?

Ich kaufe das Effizienzargument nicht. Erstens scheint es nicht so schwer, Fälle zu erkennen, in denen LET * in etwas so effizientes wie LET kompiliert werden kann. Zweitens gibt es viele Dinge in der CL-Spezifikation, die einfach nicht so aussehen, als wären sie auf Effizienz ausgelegt. (Wann haben Sie zuletzt ein LOOP mit Typdeklarationen gesehen? Das ist so schwer herauszufinden, dass ich sie noch nie gesehen habe.) Vor Dick Gabriels Benchmarks der späten 1980er Jahre war CL war wirklich langsam.

Es sieht so aus, als wäre dies ein weiterer Fall von Abwärtskompatibilität: Niemand wollte riskieren, etwas so grundlegendes wie LET zu brechen. Das war meine Vermutung, aber es ist tröstlich zu hören, dass niemand einen dumm-einfachen Fall hat, in dem ich vermisst wurde, in dem LET ein paar Dinge lächerlich einfacher machte als LET *.

79
Ken

LET selbst ist in einer Functional Programming Language kein echtes Primitiv, da es durch LAMBDA ersetzt werden kann. So was:

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

ist ähnlich wie

((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 ähnlich wie

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

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

LET erleichtert das Verständnis von Code. Man sieht eine Reihe von Bindungen und man kann jede Bindung einzeln lesen, ohne dass man den Fluss der 'Effekte' (Rebindings) von oben nach unten/links/rechts verstehen muss. Die Verwendung von LET* signalisiert dem Programmierer (derjenige, der den Code liest), dass die Bindungen nicht unabhängig sind, es gibt jedoch eine Art Top-Down-Fluss, der die Dinge kompliziert macht.

Für Common LISP gilt die Regel, dass die Werte für die Bindungen in LET von links nach rechts berechnet werden. Wie die Werte für einen Funktionsaufruf ausgewertet werden - von links nach rechts. LET ist also die konzeptionell einfachere Anweisung und sollte standardmäßig verwendet werden.

Typen in LOOP? Werden oft benutzt. Es gibt einige primitive Formen der Typdeklaration, die leicht zu merken sind. Beispiel:

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

Oben wird die Variable i als fixnum deklariert.

Richard P. Gabriel veröffentlichte 1985 sein Buch über LISP-Benchmarks, und zu dieser Zeit wurden diese Benchmarks auch bei Nicht-CL-Lisps verwendet. Common LISP selbst war 1985 brandneu - das CLtL1 Buch, in dem beschrieben wurde, dass die Sprache gerade 1984 veröffentlicht wurde. Kein Wunder, dass die Implementierungen zu dieser Zeit nicht sehr optimiert waren. Die implementierten Optimierungen waren im Wesentlichen die gleichen (oder weniger) wie die vorherigen Implementierungen (wie MacLisp). 

Für LET vs. LET* besteht der Hauptunterschied jedoch darin, dass Code, der LET verwendet, für den Menschen leichter zu verstehen ist, da die Bindungsklauseln unabhängig voneinander sind, insbesondere da es von Vorteil ist, die Auswertung von links nach rechts zu nutzen (keine Variablen setzen) als Nebeneffekt).

79
Rainer Joswig

Sie brauchen nicht LET, aber normalerweise wollen es.

LET schlägt vor, dass Sie nur eine Standard-Parallelbindung durchführen, ohne dass etwas kompliziertes passiert. LET * bewirkt Einschränkungen für den Compiler und weist den Benutzer an, dass es einen Grund dafür gibt, dass sequenzielle Bindungen erforderlich sind. In Bezug auf style ist LET besser, wenn Sie die zusätzlichen Einschränkungen von LET * nicht benötigen.

Es kann effizienter sein, LET als LET * zu verwenden (abhängig vom Compiler, Optimierer usw.):

  • parallele Bindungen können parallel ausgeführt werden (aber ich weiß nicht, ob LISP-Systeme dies tatsächlich tun, und die Init-Formulare müssen nacheinander ausgeführt werden.)
  • parallele Bindungen erstellen eine einzige neue Umgebung (Gültigkeitsbereich) für alle Bindungen. Sequentielle Bindungen erstellen eine neue verschachtelte Umgebung für jede einzelne Bindung. Parallele Bindungen verwenden weniger Speicher und haben schnelleres Nachschlagen .

(Die obigen Aufzählungspunkte beziehen sich auf Scheme, ein anderer LISP-Dialekt. Clisp kann abweichen.)

34
Mr Fooz

Ich komme mit erfundenen Beispielen. Vergleichen Sie das Ergebnis davon:

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

mit dem Ergebnis, dies auszuführen:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))
22
Logan Capaldo

In LISP besteht oft der Wunsch nach möglichst schwachen Konstrukten. In einigen Style-Guides werden Sie aufgefordert, = anstelle von eql zu verwenden, wenn Sie wissen, dass die verglichenen Elemente beispielsweise numerisch sind. Die Idee ist oft, zu definieren, was Sie meinen, anstatt den Computer effizient zu programmieren.

Es kann jedoch tatsächliche Effizienzverbesserungen geben, wenn Sie nur sagen, was Sie meinen und keine stärkeren Konstrukte verwenden. Wenn Sie mit LET Initialisierungen haben, können diese parallel ausgeführt werden, während LET*-Initialisierungen sequenziell ausgeführt werden müssen. Ich weiß nicht, ob Implementierungen dies tatsächlich tun werden, aber einige davon werden sich in der Zukunft möglicherweise gut eignen.

10
David Thornley

Der Hauptunterschied in der Common List zwischen LET und LET * besteht darin, dass Symbole in LET parallel und in LET * nacheinander gebunden sind. Die Verwendung von LET erlaubt weder die parallele Ausführung der init-forms noch die Änderung der Reihenfolge der init-forms. Der Grund ist, dass Common LISP es ermöglicht, dass Funktionen Nebenwirkungen haben. Daher ist die Reihenfolge der Auswertung wichtig und wird in einem Formular immer von links nach rechts angezeigt. In LET werden also die Init-Formulare zuerst von links nach rechts ausgewertet und dann die Bindungen erstellt. links nach rechts parallel zu. In LET * wird die Init-Form ausgewertet und dann von links nach rechts an das Symbol gebunden.

CLHS: Spezialoperator LET, LET *

9
tmh

Ich habe kürzlich eine Funktion aus zwei Argumenten geschrieben, wobei der Algorithmus am deutlichsten ausgedrückt wird, wenn wir wissen, welches Argument größer ist.

(defun foo (a b)
  (let ((a (max a b))
        (b (min a b)))
    ; here we know b is not larger
    ...)
  ; we can use the original identities of a and b here
  ; (perhaps to determine the order of the results)
  ...)

Wenn b größer wäre, hätten wir a und b versehentlich auf denselben Wert gesetzt, wenn wir let* verwendet hätten.

9

ich gehe noch einen Schritt weiter und verwende bind das vereinheitlicht let, let*, multiple-value-bind, destructuring-bind usw. und ist sogar erweiterbar.

generell mag ich die Verwendung des "schwächsten Konstrukts", aber nicht mit let & friends, da sie dem Code nur Lärm verleihen (Subjektivitätswarnung! Keine Notwendigkeit, mich vom Gegenteil zu überzeugen ...)

8
Attila Lendvai
(let ((list (cdr list))
      (pivot (car list)))
  ;quicksort
 )

Das würde natürlich funktionieren:

(let* ((rest (cdr list))
       (pivot (car list)))
  ;quicksort
 )

Und das:

(let* ((pivot (car list))
       (list (cdr list)))
  ;quicksort
 )

Aber es ist der Gedanke, der zählt.

4
Zorf

Vermutlich hat der Compiler durch die Verwendung von let mehr Flexibilität, um den Code neu anzuordnen, z. B. zur Verbesserung des Speicherplatzes oder der Geschwindigkeit.

Die Verwendung paralleler Bindungen zeigt stilistisch die Absicht, dass die Bindungen zusammen gruppiert werden; Dies wird manchmal verwendet, um dynamische Bindungen beizubehalten:

(let ((*PRINT-LEVEL* *PRINT-LEVEL*)
      (*PRINT-LENGTH* *PRINT-LENGTH*))
  (call-functions that muck with the above dynamic variables)) 
4
Doug Currie

Die OP fragt "eigentlich schon mal LET"?

Als Common LISP erstellt wurde, wurde der LISP-Code in verschiedenen Dialekten geladen. Die Leute, die Common LISP entworfen haben, haben sich darauf geeinigt, einen Dialekt von LISP zu schaffen, der Gemeinsamkeiten bietet. Sie "brauchten", um das Portieren von bestehendem Code in Common LISP einfach und attraktiv zu gestalten. Wenn Sie LET oder LET * nicht in der Sprache hinterlassen, hätte dies vielleicht andere Tugenden genutzt, aber dieses Schlüsselziel hätte ignoriert.

Ich benutze LET gegenüber LET *, weil es dem Leser etwas darüber sagt, wie sich der Datenfluss entfaltet. Wenn Sie in meinem Code ein LET * sehen, wissen Sie, dass früh gebundene Werte in einer späteren Bindung verwendet werden. Muss ich das, nein? aber ich finde es hilfreich. Ich sagte, ich habe selten Code gelesen, der standardmäßig auf LET * gesetzt ist, und das Erscheinen von LET-Signalen, dass der Autor es wirklich wollte. Das heißt Zum Beispiel, um die Bedeutung zweier Variablen zu vertauschen.

(let ((good bad)
     (bad good)
...)

Es gibt ein umstrittenes Szenario, das sich dem tatsächlichen Bedarf nähert. Es entsteht mit Makros. Dieses Makro:

(defmacro M1 (a b c)
 `(let ((a ,a)
        (b ,b)
        (c ,c))
    (f a b c)))

funktioniert besser als

(defmacro M2 (a b c)
  `(let* ((a ,a)
          (b ,b)
          (c ,c))
    (f a b c)))

da (M2 c b a) nicht klappt. Aber diese Makros sind aus verschiedenen Gründen ziemlich schlampig; so untergräbt das Argument "tatsächliche Notwendigkeit".

2
Ben Hyde

Neben Rainer Joswigs Antwort und aus puristischer oder theoretischer Sicht. Let & Let * repräsentieren zwei Programmierparadigmen; funktionell und sequentiell.

Warum sollte ich einfach Let * anstelle von Let verwenden? Nun, Sie nehmen mir den Spaß, nach Hause zu kommen und in reiner Funktionssprache zu denken, im Gegensatz zu einer sequentiellen Sprache, mit der ich den Großteil meines Tages damit arbeite :)

1
emb

Der Operator let führt eine einzige Umgebung für alle von ihm angegebenen Bindungen ein. let*, zumindest konzeptionell (und wenn wir Deklarationen für einen Moment ignorieren), werden mehrere Umgebungen eingeführt: 

Das heißt:

(let* (a b c) ...)

ist wie:

(let (a) (let (b) (let (c) ...)))

In gewisser Weise ist let primitiver, wohingegen let* ein syntaktischer Zucker ist, um eine Kaskade von lets zu schreiben.

Aber das macht nichts. (Und ich werde später begründen, warum wir "egal" sein sollten). Tatsache ist, dass es zwei Operatoren gibt und in "99%" des Codes ist es egal, welchen Sie verwenden. Der Grund, let gegenüber let* zu bevorzugen, besteht einfach darin, dass am Ende kein * baumelt.

Wann immer Sie zwei Operatoren haben und einer einen * am Namen hat, verwenden Sie den Operator ohne *, wenn dies in dieser Situation funktioniert, um den Code weniger hässlich zu halten.

Das ist alles was dazu gehört.

Ich vermute jedoch, dass, wenn let und let* ihre Bedeutungen austauschen würden, es wahrscheinlich seltener wäre, let* als jetzt zu verwenden. Das serielle Bindungsverhalten wird den meisten Code, der dies nicht erfordert, nicht stören: In seltenen Fällen müsste let* verwendet werden, um ein paralleles Verhalten anzufordern (und solche Situationen könnten auch durch Umbenennen von Variablen behoben werden, um Spiegelungen zu vermeiden).

Nun zu dieser versprochenen Diskussion. Obwohl let* konzeptionell mehrere Umgebungen einführt, ist es sehr einfach, das let*-Konstrukt so zu kompilieren, dass eine einzelne Umgebung generiert wird. Obwohl (zumindest wenn wir ANSI-CL-Deklarationen ignorieren), besteht eine algebraische Gleichheit, dass ein einzelner let* mehreren verschachtelten lets entspricht, wodurch let primitiver aussieht, Es gibt keinen Grund, let* tatsächlich in let zu erweitern, um es zu kompilieren, und sogar eine schlechte Idee.

Eine weitere Sache: Beachten Sie, dass die lambda von Common LISP let*- wie Semantik verwendet! Beispiel:

(lambda (x &optional (y x) (z (+1 y)) ...)

hier greift das x init-form für y auf den früheren x -Parameter zu, und (+1 y) verweist in ähnlicher Weise auf das frühere y -Option. In diesem Bereich der Sprache zeigt sich eine klare Präferenz für sequentielle Sichtbarkeit in der Bindung. Es wäre weniger nützlich, wenn die Formulare in lambda die Parameter nicht sehen könnten. Ein optionaler Parameter kann in Bezug auf den Wert der vorherigen Parameter nicht direkt vorgegeben werden.

1
Kaz

Mit Let können Sie die Parallelbindung verwenden,

(setq my-pi 3.1415)

(let ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3.1415)

Und mit Let * Serienbindung,

(setq my-pi 3.1415)

(let* ((my-pi 3) (old-pi my-pi))
     (list my-pi old-pi))
=> (3 3)
1
Floydan

Unter let sehen alle Variableninitialisierungsausdrücke genau dieselbe lexikalische Umgebung: die, die das let umgibt. Wenn diese Ausdrücke lexikalische Abschlüsse erfassen, können sie alle dasselbe Umgebungsobjekt verwenden.

Unter let* Befindet sich jeder Initialisierungsausdruck in einer anderen Umgebung. Für jeden nachfolgenden Ausdruck muss die Umgebung erweitert werden, um einen neuen zu erstellen. Zumindest in der abstrakten Semantik haben Abschlüsse, wenn sie erfasst werden, unterschiedliche Umgebungsobjekte.

Ein let* Muss gut optimiert sein, um die unnötigen Umgebungserweiterungen zu reduzieren, damit es sich als alltäglicher Ersatz für let eignet. Es muss einen Compiler geben, der funktioniert, welche Formulare auf was zugreifen und dann alle unabhängigen in größere, kombinierte let konvertiert.

(Dies gilt auch dann, wenn let* Nur ein Makrooperator ist, der kaskadierte let Formulare ausgibt. Die Optimierung wird für diese kaskadierten lets durchgeführt.).

Sie können let* Nicht als eine einzelne naive let mit versteckten Variablenzuweisungen implementieren, um die Initialisierungen durchzuführen, da das Fehlen eines geeigneten Gültigkeitsbereichs aufgedeckt wird:

(let* ((a (+ 2 b))  ;; b is visible in surrounding env
       (b (+ 3 a)))
  forms)

Wenn dies in gedreht wird

(let (a b)
  (setf a (+ 2 b)
        b (+ 3 a))
  forms)

in diesem Fall funktioniert es nicht. Das innere b beschattet das äußere b, sodass wir am Ende 2 zu nil addieren. Diese Art der Transformation kann durchgeführt werden, wenn wir alle diese Variablen alpha-umbenennen. Die Umgebung wird dann schön abgeflacht:

(let (#:g01 #:g02)
  (setf #:g01 (+ 2 b) ;; outer b, no problem
        #:g02 (+ 3 #:g01))
  alpha-renamed-forms) ;; a and b replaced by #:g01 and #:g02

Dazu müssen wir die Debug-Unterstützung berücksichtigen. Wenn der Programmierer mit einem Debugger in diesen lexikalischen Bereich einsteigt, sollen sie sich mit #:g01 anstelle von a befassen.

let* Ist also im Grunde genommen das komplizierte Konstrukt, das optimiert werden muss, um so gut wie let zu funktionieren, wenn es auf let reduziert werden könnte.

Das allein würde es nicht rechtfertigen, letlet* Vorzuziehen. Nehmen wir an, wir haben einen guten Compiler. warum nicht die ganze Zeit let* benutzen?

Generell sollten wir übergeordnete Konstrukte, die uns produktiv machen und Fehler reduzieren, gegenüber fehleranfälligen untergeordneten Konstrukten bevorzugen und uns so weit wie möglich auf gute Implementierungen der übergeordneten Konstrukte verlassen, damit wir selten Opfer bringen müssen ihre Verwendung zum Zwecke der Leistung. Deshalb arbeiten wir in einer Sprache wie LISP.

Diese Argumentation trifft nicht auf let im Vergleich zu let* Zu, da let* Nicht eindeutig eine Abstraktion höherer Ebene im Vergleich zu let ist. Sie sind ungefähr "gleich hoch". Mit let* Können Sie einen Bug einführen, der durch einfaches Umschalten auf let behoben wird. Und umgekehrt . let* Ist wirklich nur ein milder syntaktischer Zucker zum visuellen Zusammenbrechen von let Verschachtelungen und keine bedeutende neue Abstraktion.

0
Kaz