LET versus LET * in Common Lisp

80

Ich verstehe den Unterschied zwischen LET und LET * (parallele versus sequentielle Bindung) und theoretisch ist dies durchaus sinnvoll. Aber gibt es einen Fall, in dem Sie LET jemals wirklich gebraucht haben? In all meinem Lisp-Code, den ich mir kürzlich angesehen habe, können Sie jeden LET ohne Änderung durch LET * ersetzen.

Edit: OK, ich verstehe, warum ein Typ LET * erfunden hat, vermutlich als Makro, vor langer Zeit. Meine Frage ist, gibt es angesichts der Tatsache, dass LET * existiert, einen Grund für LET, hier zu bleiben? Haben Sie einen tatsächlichen Lisp-Code geschrieben, bei dem ein LET * nicht so gut funktioniert wie ein einfacher LET?

Ich kaufe das Effizienzargument nicht. Erstens scheint es nicht so schwierig zu sein, Fälle zu erkennen, in denen LET * zu etwas so Effizientem wie LET kompiliert werden kann. Zweitens gibt es viele Dinge in der CL-Spezifikation, die einfach nicht so aussehen, als wären sie überhaupt auf Effizienz ausgelegt. (Wann ist das letzte Mal , wenn Sie eine LOOP mit Typdeklarationen gesehen? Das sind so schwer, herauszufinden , ich habe sie nie benutzt gesehen.) Bevor Dick Gabriels Benchmarks von den späten 1980er Jahren, CL war geradezu langsam.

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

Ken
quelle
parallel ist eine schlechte Wortwahl; Es sind nur vorherige Bindungen sichtbar. Parallele Bindung wäre eher wie Haskells "... wo ..." Bindungen.
Jrockway
Ich wollte nicht verwirren; Ich glaube, das sind die Wörter, die von der Spezifikation verwendet werden. :-)
Ken
12
Parallel ist richtig. Dies bedeutet, dass die Bindungen gleichzeitig zum Leben erweckt werden und sich nicht sehen und sich nicht gegenseitig beschatten. Zu keinem Zeitpunkt existiert eine vom Benutzer sichtbare Umgebung, die einige der im LET definierten Variablen enthält, andere jedoch nicht.
Kaz
Haskells, wo Bindungen eher wie letrec sind. Sie können alle Bindungen auf derselben Bereichsebene sehen.
Edgar Klerks
1
Fragen: "Gibt es einen Fall, in dem dies leterforderlich ist?" ist ein bisschen wie die Frage "Gibt es einen Fall, in dem Funktionen mit mehr als einem Argument benötigt werden?". let& let*existieren nicht aufgrund einer Vorstellung von Effizienz, die sie existieren, weil sie es Menschen ermöglichen, beim Programmieren Absichten mit anderen Menschen zu kommunizieren.
tfb

Antworten:

85

LETselbst ist kein wirkliches Grundelement in einer funktionalen Programmiersprache , da es durch ersetzt werden kann LAMBDA. 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. LETund nicht LET*.

LETerleichtert das Verständnis des Codes. Man sieht eine Reihe von Bindungen und kann jede Bindung einzeln lesen, ohne den Fluss von 'Effekten' (Neubindungen) von oben nach unten / links nach rechts verstehen zu müssen. Verwenden von LET*Signalen an den Programmierer (der Code liest), dass die Bindungen nicht unabhängig sind, aber es gibt eine Art Top-Down-Fluss - was die Dinge kompliziert.

Common Lisp hat die Regel, dass die Werte für die Bindungen in von LETlinks nach rechts berechnet werden. Wie die Werte für einen Funktionsaufruf ausgewertet werden - von links nach rechts. Also, LETist die konzeptionell einfachere Aussage und sollte standardmäßig verwendet werden.

Tippen Sie einLOOP ? Werden ziemlich oft verwendet. 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 ials a deklariert fixnum.

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

Aber LETgegen LET*den Hauptunterschied besteht darin , dass Code LETeinfacher ist für die Menschen zu verstehen, da die Bindungsklauseln unabhängig voneinander sind - vor allem , da es schlechten Stil ist , den Vorteil des von links nach rechts Auswertung nehmen (nicht Einstellgrößen als Neben bewirken).

Rainer Joswig
quelle
3
Nein, nein! Lambda ist kein echtes Primitiv, da es durch LET und ein untergeordnetes Lambda ersetzt werden kann, das nur eine API bereitstellt, um zu den Argumentwerten zu gelangen: (low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...) :)
Kaz
36

Sie brauchen LET nicht, aber Sie wollen es normalerweise .

LET schlägt vor, dass Sie nur Standard-Parallelbindung durchführen, ohne dass etwas Kniffliges passiert. LET * führt zu Einschränkungen für den Compiler und weist den Benutzer darauf hin, dass es einen Grund gibt, warum sequentielle Bindungen erforderlich sind. In Bezug auf den Stil 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 noch nacheinander ausgeführt werden).
  • Parallele Bindungen erstellen eine einzige neue Umgebung (Bereich) für alle Bindungen. Sequentielle Bindungen erstellen für jede einzelne Bindung eine neue verschachtelte Umgebung. Parallele Bindungen verbrauchen weniger Speicher und bieten eine schnellere Suche nach Variablen .

(Die obigen Aufzählungspunkte gelten für Schema, ein anderer LISP-Dialekt. Clisp kann abweichen.)

Herr Fooz
quelle
2
Hinweis: In dieser Antwort (und / oder im verknüpften Abschnitt von Hyperspec) finden Sie eine Erklärung, warum Ihr erster Pullet-Punkt irreführend ist. Die Bindungen erfolgen parallel, aber die Formulare werden nacheinander ausgeführt - gemäß der Spezifikation.
Lindes
6
Die parallele Ausführung ist in keiner Weise vom Common Lisp-Standard geregelt. Die schnellere Suche nach Variablen ist ebenfalls ein Mythos.
Rainer Joswig
1
Der Unterschied ist nicht nur für den Compiler wichtig. Ich benutze let und let * als Hinweis darauf, was los ist. Wenn ich in meinem Code let sehe, weiß ich, dass die Bindungen unabhängig sind, und wenn ich let * sehe, weiß ich, dass die Bindungen voneinander abhängen. Aber das weiß ich nur, weil ich darauf achte, let und let * konsequent zu verwenden.
Jeremiah
28

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

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

mit dem Ergebnis dieser Ausführung:

(print (let ((c 1))
         (let* ((c 2)
                (a (+ c 1)))
           a)))
Logan Capaldo
quelle
3
Möchten Sie herausfinden, warum dies der Fall ist?
John McAleely
4
@ John: Im ersten Beispiel abezieht sich die Bindung auf den äußeren Wert von c. Im zweiten Beispiel, in let*dem sich Bindungen auf vorherige Bindungen beziehen können, abezieht sich die Bindung auf den inneren Wert von c. Logan lügt nicht darüber, 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 aFällen sollte die Bindung ein Leerzeichen über dem sein, um mit cdem von s übereinzustimmen, und der "Körper" des Inneren letsollte nur zwei Leerzeichen von sich letselbst entfernt sein.
Zem
3
Diese Antwort bietet einen wichtigen Einblick. Man würde speziell verwenden , letwenn man will vermeiden , mit sekundären Bindungen ( und damit meine ich einfach nicht den ersten) beziehen sich auf den ersten Bindung, aber Sie tun wollen ein beschatten vorherige Bindung - den vorherigen Wert davon mit zur Initialisierung eines Ihre sekundären Bindungen.
Lindes
2
Obwohl die Einrückung deaktiviert ist (und ich sie nicht bearbeiten kann), ist dies das bisher beste Beispiel. Wenn ich mir (lass ...) ansehe, weiß ich, dass sich keine der Bindungen gegenseitig aufbauen wird und separat behandelt werden kann. Wenn ich mir anschaue (lass * ...), gehe ich immer vorsichtig vor und schaue sehr genau, welche Bindungen wiederverwendet werden. Allein aus diesem Grund ist es sinnvoll, immer (let) zu verwenden, es sei denn, Sie müssen unbedingt verschachtelt werden.
Andrew
1
(6 Jahre später ...) War die falsche und irreführende Einrückung beabsichtigt, als Gotcha gedacht ? Ich bin geneigt, es zu bearbeiten, um es zu beheben ... Sollte ich nicht?
Will Ness
11

In LISP besteht häufig der Wunsch, möglichst schwache Konstrukte zu verwenden. In einigen Styleguides wird empfohlen, diese zu verwenden, =anstatt beispielsweise zu eqlwissen, dass die verglichenen Elemente numerisch sind. Die Idee ist oft, anzugeben, was Sie meinen, anstatt den Computer effizient zu programmieren.

Es kann jedoch zu tatsächlichen Effizienzverbesserungen kommen, wenn Sie nur sagen, was Sie meinen, und keine stärkeren Konstrukte verwenden. Wenn Sie Initialisierungen mit haben LET, können diese parallel ausgeführt werden, während LET*Initialisierungen nacheinander ausgeführt werden müssen. Ich weiß nicht, ob Implementierungen dies tatsächlich tun werden, aber einige könnten in Zukunft gut sein.

David Thornley
quelle
2
Guter Punkt. Da Lisp eine so hohe Sprache ist, frage ich mich nur, warum "schwächste mögliche Konstrukte" im Lisp-Land ein so gewünschter Stil sind. Sie sehen nicht , Perl - Programmierer zu sagen : „Nun, wir nicht brauchen einen regulären Ausdruck verwenden hier ...“ :-)
Ken
1
Ich weiß es nicht, aber es gibt eine bestimmte Stilpräferenz. Es wird etwas von Leuten (wie mir) abgelehnt, die so oft wie möglich dieselbe Form verwenden möchten (ich schreibe fast nie setq anstelle von setf). Es kann etwas mit einer Idee zu tun haben, zu sagen, was Sie meinen.
David Thornley
Der =Bediener ist weder stärker noch schwächer als eql. Es ist ein schwächerer Test, weil 0gleich ist 0.0. Es ist aber auch stärker, weil nicht numerische Argumente zurückgewiesen werden.
Kaz
2
Das Prinzip, auf das Sie sich beziehen, besteht darin, das stärkste anwendbare Grundelement zu verwenden, nicht das schwächste . Wenn es sich bei den verglichenen Dingen beispielsweise um Symbole handelt, verwenden Sie eq. Oder wenn Sie wissen, dass Sie einen symbolischen Ort verwenden setq. Dieses Prinzip wird jedoch auch von meinen vielen Lisp-Programmierern abgelehnt, die nur eine Hochsprache ohne vorzeitige Optimierung wollen.
Kaz
1
tatsächlich, sagt CLHS "die Bindungen parallel [fertig sind]" , sondern „die Ausdrücke init-form-1 , init-form-2 , und so weiter, in [Angabe [ausgewertet], von links nach rechts (oder oben -down)] bestellen ". Die Werte müssen also nacheinander berechnet werden (Bindungen werden hergestellt, nachdem alle Werte berechnet wurden). Sinnvoll auch, da RPLACD-ähnliche Strukturmutation Teil der Sprache ist und mit wahrer Parallelität nicht deterministisch werden würde.
Will Ness
9

Ich habe kürzlich eine Funktion aus zwei Argumenten geschrieben, bei der 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)
  ...)

Gesetzt bwar größer, wenn wir benutzt haben let*, würden wir versehentlich eingestellt haben aund bauf den gleichen Wert.

Samuel Edwin Ward
quelle
1
Wenn Sie die Werte von x und y nicht später im Inneren benötigen let, können Sie dies einfacher (und klarer) tun mit: (rotatef x y)- keine schlechte Idee, aber es scheint immer noch eine Strecke zu sein.
Ken
Das ist richtig. Es könnte nützlicher sein, wenn x und y spezielle Variablen wären.
Samuel Edwin Ward
9

Der Hauptunterschied in der gemeinsamen Liste zwischen LET und LET * besteht darin, dass Symbole in LET parallel und in LET * nacheinander gebunden werden. Die Verwendung von LET ermöglicht weder die parallele Ausführung der Init-Formulare noch die Änderung der Reihenfolge der Init-Formulare. Der Grund dafür ist, dass mit Common Lisp Funktionen Nebenwirkungen haben können. Daher ist die Reihenfolge der Bewertung wichtig und erfolgt innerhalb eines Formulars immer von links nach rechts. Somit werden in LET die Init-Formen zuerst von links nach rechts ausgewertet, dann werden die Bindungen von links nach rechts parallel erzeugt. In LET * wird die Init-Form ausgewertet und dann der Reihe nach von links nach rechts an das Symbol gebunden.

CLHS: Spezialoperator LET, LET *

tmh
quelle
2
Es scheint, als würde diese Antwort vielleicht etwas Energie daraus ziehen, eine Antwort auf diese Antwort zu sein ? Gemäß der verknüpften Spezifikation werden die Bindungen auch parallel ausgeführt LET, obwohl Sie zu Recht darauf hinweisen, dass die Init-Formulare in Reihe ausgeführt werden. Ob dies einen praktischen Unterschied in einer bestehenden Implementierung hat, weiß ich nicht.
Lindes
8

Ich gehe noch einen Schritt weiter und Verwendung binden , dass eint let, let*, multiple-value-bind, destructuring-bindusw., und es ist auch erweiterbar.

Im Allgemeinen verwende ich gerne das "schwächste Konstrukt", aber nicht mit let& friends, weil sie dem Code nur Rauschen verleihen (Subjektivitätswarnung! Sie müssen mich nicht vom Gegenteil überzeugen ...)

Attila Lendvai
quelle
Oh, ordentlich. Ich werde jetzt mit BIND spielen gehen. Danke für den Link!
Ken
4

Vermutlich hat die Verwendung letdes Compilers mehr Flexibilität, um den Code neu zu ordnen, möglicherweise für Platz- oder Geschwindigkeitsverbesserungen.

Stilistisch gesehen zeigt die Verwendung paralleler Bindungen 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)) 
Doug Currie
quelle
In 90% des Codes spielt es keine Rolle, ob Sie LEToder verwenden LET*. Wenn Sie also verwenden *, fügen Sie eine unnötige Glyphe hinzu. Wenn dies LET*der parallele Binder und LETder serielle Binder wäre, würden die Programmierer ihn weiterhin verwenden LETund nur dann herausziehen, LET*wenn sie eine parallele Bindung wünschen. Dies würde wahrscheinlich LET*selten machen .
Kaz
Tatsächlich gibt CLHS die Reihenfolge der Auswertung von Let's Init-Formularen an .
Will Ness
4
(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.

Zorf
quelle
2

Das OP fragt "jemals tatsächlich benötigt LET"?

Als Common Lisp erstellt wurde, gab es eine Schiffsladung vorhandenen Lisp-Codes in verschiedenen Dialekten. Der Auftrag, den die Leute, die Common Lisp entwarfen, akzeptierten, bestand darin, einen Lisp-Dialekt zu schaffen, der Gemeinsamkeiten bietet. Sie "brauchten", um es einfach und attraktiv zu machen, vorhandenen Code in Common Lisp zu portieren. LET oder LET * aus der Sprache herauszulassen, hätte vielleicht einigen anderen Tugenden gedient, aber dieses Schlüsselziel ignoriert.

Ich verwende LET gegenüber LET *, weil es dem Leser etwas darüber sagt, wie sich der Datenfluss entwickelt. Zumindest in meinem Code wissen Sie, wenn Sie ein LET * sehen, dass früh gebundene Werte in einer späteren Bindung verwendet werden. Muss ich das "tun", nein; aber ich denke es ist hilfreich. Das heißt, ich habe selten Code gelesen, der standardmäßig LET * ist, und das Auftreten von LET signalisiert, dass der Autor es wirklich wollte. Dh zum Beispiel, um die Bedeutung von zwei Vars zu tauschen.

(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 cba) nicht klappen wird. Aber diese Makros sind aus verschiedenen Gründen ziemlich schlampig; das untergräbt das Argument des "tatsächlichen Bedarfs".

Ben Hyde
quelle
1

Neben der Antwort von Rainer Joswig und aus puristischer oder theoretischer Sicht. Let & Let * repräsentiert zwei Programmierparadigmen; funktional bzw. sequentiell.

Warum sollte ich Let * anstelle von Let weiterhin verwenden? Nun, Sie nehmen mir den Spaß, nach Hause zu kommen und in einer reinen funktionalen Sprache zu denken, im Gegensatz zu einer sequentiellen Sprache, mit der ich den größten Teil meines Tages arbeite :)

emb
quelle
1

Mit Lassen Sie Sie parallele Bindung 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)
Floydan
quelle
Ja, so werden sie definiert. Aber wann würden Sie das erstere brauchen? Sie schreiben eigentlich kein Programm, das den Wert von pi in einer bestimmten Reihenfolge ändern muss, nehme ich an. :-)
Ken
1

Der letOperator 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 gewissem Sinne letist es primitiver, während let*es sich um einen syntaktischen Zucker zum Schreiben einer Kaskade von let-s handelt.

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

Wenn Sie zwei Operatoren haben und einer *auf dem Namen baumelt, verwenden Sie den Operator ohne, *wenn dies in dieser Situation funktioniert, um Ihren Code weniger hässlich zu halten.

Das ist alles was dazu gehört.

Abgesehen davon vermute ich, dass es wahrscheinlich seltener wäre, es zu verwenden , wenn letund wenn let*ihre Bedeutungen ausgetauscht würden, let*als jetzt. Das serielle Bindungsverhalten stört den meisten Code, der es nicht benötigt, selten: Es müsste selten let*verwendet werden, um paralleles Verhalten anzufordern (und solche Situationen könnten auch durch Umbenennen von Variablen behoben werden, um Schattenbildung zu vermeiden).

Nun zu dieser versprochenen Diskussion. Obwohl let*konzeptionell mehrere Umgebungen eingeführt werden, ist es sehr einfach, das let*Konstrukt so zu kompilieren , dass eine einzelne Umgebung generiert wird. Also auch wenn (zumindest , wenn wir ignorieren ANSI CL Erklärungen), gibt es eine algebraische Gleichheit , die ein einzelnes let*entspricht mehrere verschachtelte let-s, das macht letprimitiveren aussehen, gibt es keinen Grund, tatsächlich zu erweitern let*in letes zu kompilieren, und sogar eine schlechte Idee.

Noch etwas: Beachten Sie, dass Common Lisp lambdatatsächlich eine let*ähnliche Semantik verwendet! Beispiel:

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

Hier bezieht sich das xInit-Formular für den yZugriff auf den früheren xParameter und (+1 y)verweist in ähnlicher Weise auf das frühere yoptionale. In diesem Bereich der Sprache zeigt sich eine klare Präferenz für sequentielle Sichtbarkeit bei der Bindung. Es wäre weniger nützlich, wenn die Formulare in a lambdadie Parameter nicht sehen könnten. Ein optionaler Parameter konnte in Bezug auf den Wert der vorherigen Parameter nicht prägnant voreingestellt werden.

Kaz
quelle
0

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

Unter let*befindet sich jeder initialisierende Ausdruck 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 unterschiedliche Umgebungsobjekte, wenn sie erfasst werden.

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

(Dies gilt auch dann, wenn let*es sich nur um einen Makrooperator handelt, der kaskadierte letFormulare ausgibt . Die Optimierung erfolgt für diese kaskadierten lets).

Sie können nicht let*als einzelne Naive letmit versteckten Variablenzuweisungen implementieren , um die Initialisierungen durchzuführen , da das Fehlen eines ordnungsgemäßen Gültigkeitsbereichs aufgedeckt wird:

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

Wenn dies in verwandelt wird

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

es wird in diesem Fall nicht funktionieren; Das Innere bbeschattet das Äußere, bso dass wir am Ende 2 hinzufügen nil. Diese Art der Transformation kann durchgeführt werden, wenn alle diese Variablen in Alpha umbenannt werden. Die Umgebung ist 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

Dafür müssen wir die Debug-Unterstützung berücksichtigen. Wenn der Programmierer mit einem Debugger in diesen lexikalischen Bereich eintritt, möchten wir, dass er sich #:g01stattdessen mit ihnen befasst a.

Im Grunde genommen let*handelt es sich also um das komplizierte Konstrukt, das sowohl für die Leistung als auch letin Fällen, in denen es reduziert werden könnte , gut optimiert werden muss let.

Das allein würde die Begünstigung nicht rechtfertigen letüber let*. Nehmen wir an, wir haben einen guten Compiler. warum nicht die let*ganze Zeit verwenden?

Grundsätzlich sollten wir Konstrukte auf höherer Ebene, die uns produktiv machen und Fehler reduzieren, gegenüber fehleranfälligen Konstrukten auf niedrigerer Ebene bevorzugen und uns so weit wie möglich auf gute Implementierungen der Konstrukte auf höherer Ebene verlassen, damit wir selten Opfer bringen müssen ihre Verwendung für die Leistung. Deshalb arbeiten wir in erster Linie in einer Sprache wie Lisp.

Diese Argumentation trifft nicht gut auf letversus zu let*, da let*es sich nicht eindeutig um eine Abstraktion auf höherer Ebene handelt let. Sie sind ungefähr "gleichwertig". Mit let*können Sie einen Fehler einführen, der durch einfaches Umschalten behoben wird let. Und umgekehrt . let*ist wirklich nur ein milder syntaktischer Zucker zum visuell kollabierenden letVerschachteln und keine signifikante neue Abstraktion.

Kaz
quelle
-1

Ich verwende meistens LET, es sei denn, ich benötige speziell LET *, aber manchmal schreibe ich Code, der explizit LET benötigt , normalerweise, wenn verschiedene (normalerweise komplizierte) Standardeinstellungen vorgenommen werden. Leider habe ich kein handliches Codebeispiel zur Hand.

Vatine
quelle
-1

Wer hat Lust, letf vs letf * noch einmal neu zu schreiben? die Anzahl der Abwicklungsschutzanrufe?

einfacher, sequentielle Bindungen zu optimieren.

Vielleicht wirkt es sich auf die Umgebung aus ?

erlaubt Fortsetzungen mit dynamischem Ausmaß?

manchmal (let (xyz) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ xy) (setq x (1- x))))) (values ​​()))

[Ich denke, das funktioniert] Punkt ist, einfacher ist manchmal leichter zu lesen.

Steve
quelle