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 *.
quelle
let
erforderlich 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.Antworten:
LET
selbst ist kein wirkliches Grundelement in einer funktionalen Programmiersprache , da es durch ersetzt werden kannLAMBDA
. 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 nichtLET*
.LET
erleichtert 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 vonLET*
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
LET
links nach rechts berechnet werden. Wie die Werte für einen Funktionsaufruf ausgewertet werden - von links nach rechts. Also,LET
ist die konzeptionell einfachere Aussage und sollte standardmäßig verwendet werden.Tippen Sie ein
LOOP
? 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
i
als a deklariertfixnum
.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
LET
gegenLET*
den Hauptunterschied besteht darin , dass CodeLET
einfacher 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).quelle
(low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...)
:)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.):
(Die obigen Aufzählungspunkte gelten für Schema, ein anderer LISP-Dialekt. Clisp kann abweichen.)
quelle
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)))
quelle
a
bezieht sich die Bindung auf den äußeren Wert vonc
. Im zweiten Beispiel, inlet*
dem sich Bindungen auf vorherige Bindungen beziehen können,a
bezieht sich die Bindung auf den inneren Wert vonc
. 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 beidena
Fällen sollte die Bindung ein Leerzeichen über dem sein, um mitc
dem von s übereinzustimmen, und der "Körper" des Innerenlet
sollte nur zwei Leerzeichen von sichlet
selbst entfernt sein.let
wenn 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.In LISP besteht häufig der Wunsch, möglichst schwache Konstrukte zu verwenden. In einigen Styleguides wird empfohlen, diese zu verwenden,
=
anstatt beispielsweise zueql
wissen, 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ährendLET*
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.quelle
=
Bediener ist weder stärker noch schwächer alseql
. Es ist ein schwächerer Test, weil0
gleich ist0.0
. Es ist aber auch stärker, weil nicht numerische Argumente zurückgewiesen werden.eq
. Oder wenn Sie wissen, dass Sie einen symbolischen Ort verwendensetq
. Dieses Prinzip wird jedoch auch von meinen vielen Lisp-Programmierern abgelehnt, die nur eine Hochsprache ohne vorzeitige Optimierung wollen.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
b
war größer, wenn wir benutzt habenlet*
, würden wir versehentlich eingestellt habena
undb
auf den gleichen Wert.quelle
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.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 rechtsparallel 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 *
quelle
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.Ich gehe noch einen Schritt weiter und Verwendung binden , dass eint
let
,let*
,multiple-value-bind
,destructuring-bind
usw., 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 ...)quelle
Vermutlich hat die Verwendung
let
des 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))
quelle
LET
oder verwendenLET*
. Wenn Sie also verwenden*
, fügen Sie eine unnötige Glyphe hinzu. Wenn diesLET*
der parallele Binder undLET
der serielle Binder wäre, würden die Programmierer ihn weiterhin verwendenLET
und nur dann herausziehen,LET*
wenn sie eine parallele Bindung wünschen. Dies würde wahrscheinlichLET*
selten machen .(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.
quelle
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".
quelle
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 :)
quelle
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)
quelle
Der
let
Operator 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
let
ist es primitiver, währendlet*
es sich um einen syntaktischen Zucker zum Schreiben einer Kaskade vonlet
-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
überlet*
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
let
und wennlet*
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 seltenlet*
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, daslet*
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 einzelneslet*
entspricht mehrere verschachteltelet
-s, das machtlet
primitiveren aussehen, gibt es keinen Grund, tatsächlich zu erweiternlet*
inlet
es zu kompilieren, und sogar eine schlechte Idee.Noch etwas: Beachten Sie, dass Common Lisp
lambda
tatsächlich einelet*
ähnliche Semantik verwendet! Beispiel:(lambda (x &optional (y x) (z (+1 y)) ...)
Hier bezieht sich das
x
Init-Formular für deny
Zugriff auf den früherenx
Parameter und(+1 y)
verweist in ähnlicher Weise auf das früherey
optionale. 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 alambda
die Parameter nicht sehen könnten. Ein optionaler Parameter konnte in Bezug auf den Wert der vorherigen Parameter nicht prägnant voreingestellt werden.quelle
Unter sehen
let
alle Variableninitialisierungsausdrücke genau dieselbe lexikalische Umgebung: die, die die Umgebung umgibtlet
. 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 seinlet
. Es muss einen Compiler geben, der funktioniert, welche Formulare auf was zugreifen und dann alle unabhängigen in größere, kombinierte konvertierenlet
.(Dies gilt auch dann, wenn
let*
es sich nur um einen Makrooperator handelt, der kaskadiertelet
Formulare ausgibt . Die Optimierung erfolgt für diese kaskadiertenlet
s).Sie können nicht
let*
als einzelne Naivelet
mit 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
b
beschattet das Äußere,b
so dass wir am Ende 2 hinzufügennil
. 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
#:g01
stattdessen mit ihnen befassta
.Im Grunde genommen
let*
handelt es sich also um das komplizierte Konstrukt, das sowohl für die Leistung als auchlet
in Fällen, in denen es reduziert werden könnte , gut optimiert werden musslet
.Das allein würde die Begünstigung nicht rechtfertigen
let
überlet*
. Nehmen wir an, wir haben einen guten Compiler. warum nicht dielet*
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
let
versus zulet*
, dalet*
es sich nicht eindeutig um eine Abstraktion auf höherer Ebene handeltlet
. Sie sind ungefähr "gleichwertig". Mitlet*
können Sie einen Fehler einführen, der durch einfaches Umschalten behoben wirdlet
. Und umgekehrt .let*
ist wirklich nur ein milder syntaktischer Zucker zum visuell kollabierendenlet
Verschachteln und keine signifikante neue Abstraktion.quelle
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.
quelle
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.
quelle