Entiendo la diferencia entre LET y LET * (enlace paralelo versus secuencial) y, como cuestión teórica, tiene mucho sentido. Pero, ¿hay algún caso en el que alguna vez hayas necesitado LET? En todo mi código Lisp que he visto recientemente, podría reemplazar cada LET con LET * sin cambios.
Editar: OK, entiendo por qué un tipo inventó LET *, presumiblemente como una macro, hace mucho tiempo. Mi pregunta es, dado que LET * existe, ¿hay alguna razón para que LET se quede? ¿Ha escrito algún código Lisp real donde un LET * no funcionaría tan bien como un LET simple?
No compro el argumento de la eficiencia. Primero, reconocer los casos en los que LET * se puede compilar en algo tan eficiente como LET no parece tan difícil. En segundo lugar, hay muchas cosas en la especificación CL que simplemente no parecen haber sido diseñadas en función de la eficiencia. (¿Cuándo fue la última vez que vio un LOOP con declaraciones de tipos? Son tan difíciles de entender que nunca los había visto usar). Antes de los puntos de referencia de Dick Gabriel de finales de los 80, CL era francamente lento.
Parece que este es otro caso de compatibilidad con versiones anteriores: sabiamente, nadie quería arriesgarse a romper algo tan fundamental como LET. Esa fue mi corazonada, pero es reconfortante escuchar que nadie tiene un caso estúpidamente simple que me faltaba donde LET hizo un montón de cosas ridículamente más fáciles que LET *.
fuente
let
se necesite?' es un poco como preguntar '¿hay algún caso en el que se necesiten funciones con más de un argumento?'.let
ylet*
no existen debido a alguna noción de eficiencia, existen porque permiten a los humanos comunicar la intención a otros humanos al programar.Respuestas:
LET
en sí mismo no es un primitivo real en un lenguaje de programación funcional , ya que se puede reemplazar conLAMBDA
. Me gusta esto:(let ((a1 b1) (a2 b2) ... (an bn)) (some-code a1 a2 ... an))
es parecido a
((lambda (a1 a2 ... an) (some-code a1 a2 ... an)) b1 b2 ... bn)
Pero
(let* ((a1 b1) (a2 b2) ... (an bn)) (some-code a1 a2 ... an))
es parecido a
((lambda (a1) ((lambda (a2) ... ((lambda (an) (some-code a1 a2 ... an)) bn)) b2)) b1)
Puedes imaginar cuál es la cosa más sencilla.
LET
y noLET*
.LET
facilita la comprensión del código. Uno ve un montón de enlaces y uno puede leer cada enlace individualmente sin la necesidad de comprender el flujo de arriba hacia abajo / izquierda-derecha de los 'efectos' (rebindings). UsarLET*
señales para el programador (el que lee el código) de que los enlaces no son independientes, pero hay algún tipo de flujo de arriba hacia abajo, lo que complica las cosas.Common Lisp tiene la regla de que los valores de los enlaces
LET
se calculan de izquierda a derecha. Exactamente cómo se evalúan los valores para una llamada de función: de izquierda a derecha. Entonces,LET
es la declaración conceptualmente más simple y debería usarse por defecto.Tipos en
LOOP
? Se utilizan con bastante frecuencia. Hay algunas formas primitivas de declaración de tipos que son fáciles de recordar. Ejemplo:(LOOP FOR i FIXNUM BELOW (TRUNCATE n 2) do (something i))
Arriba declara que la variable
i
es afixnum
.Richard P. Gabriel publicó su libro sobre los puntos de referencia Lisp en 1985 y en ese momento estos puntos de referencia también se utilizaban con Lisps que no eran de CL. Common Lisp en sí era nuevo en 1985: el libro CLtL1 que describía el lenguaje se acababa de publicar en 1984. No es de extrañar que las implementaciones no estuvieran muy optimizadas en ese momento. Las optimizaciones implementadas fueron básicamente las mismas (o menos) que las implementaciones anteriores (como MacLisp).
Pero para
LET
vs.,LET*
la principal diferencia es que el uso de códigoLET
es más fácil de entender para los humanos, ya que las cláusulas vinculantes son independientes entre sí, especialmente porque es de mal estilo aprovechar la evaluación de izquierda a derecha (no establecer variables como un lado efecto).fuente
(low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...)
:)No necesitas LET, pero normalmente lo quieres .
LET sugiere que solo está haciendo un enlace paralelo estándar sin nada complicado. LET * induce restricciones en el compilador y sugiere al usuario que existe una razón por la que se necesitan enlaces secuenciales. En términos de estilo , LET es mejor cuando no necesita las restricciones adicionales impuestas por LET *.
Puede ser más eficiente usar LET que LET * (dependiendo del compilador, optimizador, etc.):
(Las viñetas anteriores se aplican a Scheme, otro dialecto LISP. Clisp puede diferir).
fuente
Vengo con ejemplos artificiales. Compare el resultado de esto:
(print (let ((c 1)) (let ((c 2) (a (+ c 1))) a)))
con el resultado de ejecutar esto:
(print (let ((c 1)) (let* ((c 2) (a (+ c 1))) a)))
fuente
a
el enlace 'se refiere al valor externo dec
. En el segundo ejemplo, dondelet*
permite que los enlaces se refieran a enlaces anteriores,a
el enlace se refiere al valor interno dec
. Logan no miente acerca de que este sea un ejemplo artificial y ni siquiera pretende ser útil. Además, la sangría no es estándar y es engañosa. En ambos,a
la encuadernación de 'debe estar un espacio encima, para alinearse conc
' s, y el 'cuerpo' de la parte internalet
debe estar a solo dos espacios dellet
mismo.let
cuando se quiere evitar tener fijaciones secundarias (y me refiero simplemente no el primero) se refieren a la primera unión, pero qué desea sombra de una unión anterior - con el valor anterior de la misma para la inicialización de una de tus enlaces secundarios.En LISP, a menudo existe el deseo de utilizar las construcciones más débiles posibles. Algunas guías de estilo le dirán que use en
=
lugar deeql
cuando sepa que los elementos comparados son numéricos, por ejemplo. La idea suele ser especificar lo que quiere decir en lugar de programar la computadora de manera eficiente.Sin embargo, puede haber mejoras de eficiencia reales al decir solo lo que quiere decir y no usar construcciones más fuertes. Si tiene inicializaciones con
LET
, se pueden ejecutar en paralelo, mientras que lasLET*
inicializaciones deben ejecutarse secuencialmente. No sé si alguna implementación realmente hará eso, pero es posible que algunas en el futuro.fuente
=
operador no es ni más fuerte ni más débil queeql
. Es una prueba más débil porque0
es igual a0.0
. Pero también es más fuerte porque se rechazan los argumentos no numéricos.eq
. O si sabe que le está asignando un uso a un lugar simbólicosetq
. Sin embargo, este principio también es rechazado por muchos programadores Lisp, que solo quieren un lenguaje de alto nivel sin optimización prematura.Recientemente escribí una función de dos argumentos, donde el algoritmo se expresa con mayor claridad si sabemos qué argumento es más grande.
(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) ...)
Suponiendo
b
era más grande, si habíamos utilizadolet*
, habríamos establecido por accidentea
yb
en el mismo valor.fuente
let
, esto se puede hacer de manera más simple (y clara) con:(rotatef x y)
- no es una mala idea, pero todavía parece una exageración.La principal diferencia en la lista común entre LET y LET * es que los símbolos en LET están vinculados en paralelo y en LET * están vinculados secuencialmente. El uso de LET no permite que los formularios de inicio se ejecuten en paralelo ni permite cambiar el orden de los formularios de inicio. La razón es que Common Lisp permite que las funciones tengan efectos secundarios. Por lo tanto, el orden de evaluación es importante y siempre es de izquierda a derecha dentro de un formulario. Por lo tanto, en LET, las formas de inicio se evalúan primero, de izquierda a derecha, luego se crean los enlaces, de
izquierda a derechaen paralelo. En LET *, la forma init se evalúa y luego se enlaza con el símbolo en secuencia, de izquierda a derecha.CLHS: Operador especial LET, LET *
fuente
LET
, aunque tiene razón al decir que los formularios de inicio se ejecutan en serie. Si eso tiene alguna diferencia práctica en cualquier implementación existente, no lo sé.i ir un paso más allá y uso bind que unifica
let
,let*
,multiple-value-bind
,destructuring-bind
etc, y es aún más extensible.generalmente me gusta usar el "constructo más débil", pero no con
let
& friends porque simplemente hacen ruido al código (¡advertencia de subjetividad! No es necesario que intentes convencerme de lo contrario ...)fuente
Presumiblemente usando
let
el compilador se tiene más flexibilidad para reordenar el código, quizás para mejorar el espacio o la velocidad.Estilísticamente, el uso de encuadernaciones paralelas muestra la intención de que las encuadernaciones estén agrupadas; esto a veces se usa para retener enlaces dinámicos:
(let ((*PRINT-LEVEL* *PRINT-LEVEL*) (*PRINT-LENGTH* *PRINT-LENGTH*)) (call-functions that muck with the above dynamic variables))
fuente
LET
oLET*
. Entonces, si lo usa*
, está agregando un glifo innecesario. SiLET*
fuera el encuadernador paralelo yLET
fuera el encuadernador en serie, los programadores aún lo usaríanLET
y solo lo sacaríanLET*
cuando quisieran el encuadernado paralelo. Esto probablemente lo haríaLET*
raro.(let ((list (cdr list)) (pivot (car list))) ;quicksort )
Por supuesto, esto funcionaría:
(let* ((rest (cdr list)) (pivot (car list))) ;quicksort )
Y esto:
(let* ((pivot (car list)) (list (cdr list))) ;quicksort )
Pero es el pensamiento lo que cuenta.
fuente
El OP pregunta "¿alguna vez necesitó LET"?
Cuando se creó Common Lisp, había una gran cantidad de código Lisp existente en varios dialectos. El mandato que aceptaron las personas que diseñaron Common Lisp fue crear un dialecto de Lisp que proporcionaría un terreno común. "Necesitaban" hacer fácil y atractivo portar el código existente a Common Lisp. Dejar LET o LET * fuera del idioma podría haber servido para otras virtudes, pero habría ignorado ese objetivo clave.
Utilizo LET en lugar de LET * porque le dice al lector algo sobre cómo se está desarrollando el flujo de datos. En mi código, al menos, si ve un LET *, sabe que los valores enlazados temprano se usarán en un enlace posterior. "Necesito" hacer eso, no; pero creo que es útil. Dicho esto, he leído, en raras ocasiones, un código que por defecto es LET * y la aparición de LET indica que el autor realmente lo quería. Es decir, por ejemplo para intercambiar el significado de dos vars.
(let ((good bad) (bad good) ...)
Hay un escenario discutible que se acerca a la "necesidad real". Surge con macros. Esta macro:
(defmacro M1 (a b c) `(let ((a ,a) (b ,b) (c ,c)) (f a b c)))
funciona mejor que
(defmacro M2 (a b c) `(let* ((a ,a) (b ,b) (c ,c)) (f a b c)))
ya que (M2 cba) no va a funcionar. Pero esas macros son bastante descuidadas por diversas razones; por lo que socava el argumento de la "necesidad real".
fuente
Además de la respuesta de Rainer Joswig , y desde un punto de vista purista o teórico. Sea & Let * representar dos paradigmas de programación; funcional y secuencial respectivamente.
En cuanto a por qué debería seguir usando Let * en lugar de Let, bueno, me estás quitando la diversión al volver a casa y pensar en un lenguaje funcional puro, en lugar del lenguaje secuencial con el que paso la mayor parte del día trabajando :)
fuente
Con Permitir usar enlace paralelo,
(setq my-pi 3.1415) (let ((my-pi 3) (old-pi my-pi)) (list my-pi old-pi)) => (3 3.1415)
Y con Let * enlace en serie,
(setq my-pi 3.1415) (let* ((my-pi 3) (old-pi my-pi)) (list my-pi old-pi)) => (3 3)
fuente
El
let
operador introduce un único entorno para todos los enlaces que especifica.let*
, al menos conceptualmente (y si ignoramos las declaraciones por un momento) introduce múltiples entornos:Es decir:
(let* (a b c) ...)
es como:
(let (a) (let (b) (let (c) ...)))
Entonces, en cierto sentido,
let
es más primitivo, mientras quelet*
es un azúcar sintáctico para escribir una cascada delet
-s.Pero eso no importa. (Y daré una justificación más adelante a continuación de por qué no deberíamos "no importarnos"). El hecho es que hay dos operadores, y en el "99%" del código, no importa cuál use. La razón para preferir
let
máslet*
es simplemente que no tiene la*
cuelga al final.Siempre que tenga dos operadores, y uno tenga un
*
nombre colgando, use el que no tiene*
si funciona en esa situación, para mantener su código menos feo.Eso es todo lo que hay que hacer.
Dicho esto, sospecho que si
let
elet*
intercambiaran sus significados, probablemente sería más raro de usarlet*
que ahora. El comportamiento de enlace serial no molestará a la mayoría de los códigos que no lo requieran: rara vezlet*
tendría que usar para solicitar un comportamiento en paralelo (y estas situaciones también podrían solucionarse cambiando el nombre de las variables para evitar el sombreado).Ahora para la discusión prometida. Aunque
let*
conceptualmente presenta múltiples entornos, es muy fácil compilar lalet*
construcción para que se genere un solo entorno. Así que a pesar de que (al menos si hacemos caso de las declaraciones ANSI CL), existe una igualdad algebraica que un sololet*
corresponde a múltiples anidadaslet
-s, lo que hace quelet
se parecen más primitiva, no hay razón para realmente expandirlet*
enlet
compilarlo, e incluso una mala idea.Una cosa más: tenga en cuenta que Common Lisp en
lambda
realidad usalet*
semántica similar a la Ejemplo:(lambda (x &optional (y x) (z (+1 y)) ...)
aquí, el
x
formulario init paray
accede alx
parámetro anterior , y de manera similar se(+1 y)
refiere aly
opcional anterior . En esta área del lenguaje, se manifiesta una clara preferencia por la visibilidad secuencial en la encuadernación. Sería menos útil si los formularios en alambda
no pudieran ver los parámetros; un parámetro opcional no se pudo establecer de forma sucinta por defecto en términos del valor de los parámetros anteriores.fuente
Debajo
let
, todas las expresiones de inicialización de variables ven exactamente el mismo entorno léxico: el que rodea allet
. Si esas expresiones capturan cierres léxicos, todas pueden compartir el mismo objeto de entorno.En
let*
, cada expresión de inicialización se encuentra en un entorno diferente. Para cada expresión sucesiva, el entorno debe ampliarse para crear uno nuevo. Al menos en la semántica abstracta, si se capturan cierres, tienen diferentes objetos de entorno.A
let*
debe estar bien optimizado para colapsar las extensiones de entorno innecesarias con el fin de adaptarse como un reemplazo diario paralet
. Tiene que haber un compilador que funcione qué formularios están accediendo a qué y luego convierte todos los independientes en más grandes, combinadoslet
.(Esto es cierto incluso si
let*
es solo un operador de macro que emitelet
formas en cascada ; la optimización se realiza en laslet
s en ).No puede implementar
let*
como un solo ingenuolet
, con asignaciones de variables ocultas para hacer las inicializaciones porque se revelará la falta de un alcance adecuado:(let* ((a (+ 2 b)) ;; b is visible in surrounding env (b (+ 3 a))) forms)
Si esto se convierte en
(let (a b) (setf a (+ 2 b) b (+ 3 a)) forms)
no funcionará en este caso; el interior
b
está sombreando al exterior,b
por lo que terminamos sumando 2 anil
. Este tipo de transformación se puede hacer si cambiamos el nombre alfa de todas estas variables. A continuación, el entorno se aplana agradablemente:(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
Para eso debemos considerar el soporte de depuración; Si el programador entra en este ámbito léxico con un depurador, ¿queremos que se ocupen de ellos en
#:g01
lugar dea
.Entonces, básicamente,
let*
es la construcción complicada la que debe optimizarse bien para funcionar tan bien comolet
en los casos en que podría reducirse alet
.Eso por sí solo no justificaría que favorece
let
máslet*
. Supongamos que tenemos un buen compilador; ¿por qué no usarlet*
todo el tiempo?Como principio general, deberíamos favorecer las construcciones de nivel superior que nos hacen productivos y reducen los errores, sobre las construcciones de nivel inferior propensas a errores y depender tanto como sea posible de buenas implementaciones de las construcciones de nivel superior para que rara vez tengamos que sacrificar su uso en aras del rendimiento. Es por eso que estamos trabajando en un lenguaje como Lisp en primer lugar.
Ese razonamiento no se aplica muy bien a
let
versuslet*
, porquelet*
claramente no es una abstracción de nivel superior en relación conlet
. Son de "nivel igual". Conlet*
, puede introducir un error que se resuelve simplemente cambiando alet
. Y viceversa .let*
realmente es solo un azúcar sintáctico suave para ellet
anidamiento que colapsa visualmente , y no una nueva abstracción significativa.fuente
Principalmente uso LET, a menos que necesite específicamente LET *, pero a veces escribo código que explícitamente necesita LET, generalmente cuando hago una variedad (generalmente complicada) por defecto. Desafortunadamente, no tengo ningún ejemplo de código útil a mano.
fuente
¿Quién tiene ganas de volver a escribir letf vs letf *? el número de llamadas de protección de desconexión?
más fácil de optimizar los enlaces secuenciales.
tal vez afecte al env ?
permite continuaciones con extensión dinámica?
a veces (let (xyz) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ xy) (setq x (1- x))))) (valores ()))
[Creo que funciona] el punto es que, a veces, lo más simple es más fácil de leer.
fuente