LET versus LET * en Common Lisp


80

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 *.


paralelo es una mala elección de palabras; solo las vinculaciones anteriores son visibles. El enlace paralelo sería más como el enlace "... donde ..." de Haskell.
Jrockway

No pretendía confundir; Creo que esas son las palabras utilizadas por la especificación. :-)
Ken

12
Paralelo es correcto. Significa que las uniones cobran vida al mismo tiempo y no se ven ni se ensombrecen entre sí. En ningún momento existe un entorno visible para el usuario que incluya algunas de las variables definidas en el LET, pero no otras.
Kaz

Haskells donde las uniones son más como letrec. Pueden ver todas las vinculaciones en el mismo nivel de alcance.
Edgar Klerks

1
Preguntando '¿hay algún caso donde letse necesite?' es un poco como preguntar '¿hay algún caso en el que se necesiten funciones con más de un argumento?'. lety let*no existen debido a alguna noción de eficiencia, existen porque permiten a los humanos comunicar la intención a otros humanos al programar.
tfb

Respuestas:


85

LETen sí mismo no es un primitivo real en un lenguaje de programación funcional , ya que se puede reemplazar con LAMBDA. 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. LETy no LET*.

LETfacilita 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). Usar LET*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 LETse calculan de izquierda a derecha. Exactamente cómo se evalúan los valores para una llamada de función: de izquierda a derecha. Entonces, LETes la declaración conceptualmente más simple y debería usarse por defecto.

Tipos enLOOP ? 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 ies a fixnum.

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 LETvs., LET*la principal diferencia es que el uso de código LETes 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).


3
¡No no! Lambda no es una primitiva real porque se puede reemplazar con LET y una lambda de nivel inferior que solo proporciona una API para obtener los valores de los argumentos: (low-level-lambda 2 (let ((x (car %args%)) (y (cadr args))) ...) :)
Kaz

36

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.):

  • los enlaces paralelos se pueden ejecutar en paralelo (pero no sé si algún sistema LISP realmente hace esto, y los formularios de inicio aún deben ejecutarse secuencialmente)
  • Los enlaces paralelos crean un único entorno nuevo (ámbito) para todos los enlaces. Los enlaces secuenciales crean un nuevo entorno anidado para cada enlace. Los enlaces paralelos utilizan menos memoria y tienen una búsqueda de variables más rápida .

(Las viñetas anteriores se aplican a Scheme, otro dialecto LISP. Clisp puede diferir).


2
Nota: Vea esta respuesta (y / o la sección vinculada de hiperespecificación) para obtener una pequeña explicación de por qué su primer punto de pollita es engañoso, digamos. Las vinculaciones ocurren en paralelo, pero los formularios se ejecutan secuencialmente, según la especificación.
lindes

6
la ejecución paralela no es algo de lo que se ocupa el estándar Common Lisp de ninguna manera. La búsqueda de variables más rápida también es un mito.
Rainer Joswig

1
La diferencia no solo es importante para el compilador. Utilizo let y let * como indicios de lo que está sucediendo. Cuando veo let en mi código, sé que los enlaces son independientes, y cuando veo let *, sé que los enlaces dependen unos de otros. Pero solo lo sé porque me aseguro de usar let y let * consistentemente.
Jeremías

28

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)))

3
¿Te importa desarrollar por qué este es el caso?
John McAleely

4
@John: en el primer ejemplo, ael enlace 'se refiere al valor externo de c. En el segundo ejemplo, donde let*permite que los enlaces se refieran a enlaces anteriores, ael enlace se refiere al valor interno de c. 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, ala encuadernación de 'debe estar un espacio encima, para alinearse con c' s, y el 'cuerpo' de la parte interna letdebe estar a solo dos espacios del letmismo.
zem

3
Esta respuesta proporciona una información importante. Uno podría específicamente utilizarlet 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.
lindes

2
Aunque la sangría está desactivada (y no puedo editarla), este es el mejor ejemplo hasta ahora. Cuando miro (deja ...) sé que ninguno de los enlaces se va a construir entre sí y se puede tratar por separado. Cuando miro (deje * ...) siempre me acerco con precaución y miro con mucho cuidado qué enlaces se están reutilizando. Solo por esta razón, tiene sentido usar siempre (let) a menos que sea absolutamente necesario anidar.
andrew

1
(6 años más tarde ...) ¿fue la sangría incorrecta y engañosa por diseño, pensada como una trampa ? Me inclino a editarlo para solucionarlo ... ¿No debería?
Will Ness

11

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 las LET*inicializaciones deben ejecutarse secuencialmente. No sé si alguna implementación realmente hará eso, pero es posible que algunas en el futuro.


2
Buen punto. Aunque como Lisp es un lenguaje de tan alto nivel, eso me hace preguntarme por qué "las construcciones más débiles posibles" es un estilo tan deseado en Lisp land. No ves a los programadores de Perl diciendo "bueno, no necesitamos usar una expresión regular aquí ..." :-)
Ken

1
No lo sé, pero hay una preferencia de estilo definida. Se opone de alguna manera a personas (como yo) a quienes les gusta usar la misma forma tanto como sea posible (casi nunca escribo setq en lugar de setf). Puede tener algo que ver con una idea para decir lo que quieres decir.
David Thornley

El =operador no es ni más fuerte ni más débil que eql. Es una prueba más débil porque 0es igual a 0.0. Pero también es más fuerte porque se rechazan los argumentos no numéricos.
Kaz

2
El principio al que se refiere es utilizar el primitivo aplicable más fuerte , no el más débil . Por ejemplo, si las cosas que se comparan son símbolos, utilice eq. O si sabe que le está asignando un uso a un lugar simbólico setq. Sin embargo, este principio también es rechazado por muchos programadores Lisp, que solo quieren un lenguaje de alto nivel sin optimización prematura.
Kaz

1
en realidad, CLHS dice "los enlaces [se hacen] en paralelo" pero "las expresiones init-form-1 , init-form-2 , etc., [se evalúan] en [específico, de izquierda a derecha (o arriba -down)] orden ". Por lo tanto, los valores deben calcularse secuencialmente (los enlaces se establecen después de que se hayan calculado todos los valores). También tiene sentido, ya que la mutación de estructura similar a RPLACD es parte del lenguaje y con un verdadero paralelismo se volvería no determinista.
Will Ness

9

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 bera más grande, si habíamos utilizado let*, habríamos establecido por accidente ay ben el mismo valor.


1
A menos que necesite los valores de xey más adelante dentro del exterior 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.
Ken

Es verdad. Podría ser más útil si xey fueran variables especiales.
Samuel Edwin Ward

9

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 derecha en 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 *


2
¿Parece que esta respuesta quizás esté extrayendo algo de energía al ser una respuesta a esta respuesta ? Además, según la especificación vinculada, se dice que los enlaces se realizan en paralelo 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é.
lindes

8

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 ...)


Ooh, genial. Voy a jugar con BIND ahora. Gracias por el enlace!
Ken

4

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)) 

En el 90% del código, no importa si usa LETo LET*. Entonces, si lo usa *, está agregando un glifo innecesario. Si LET*fuera el encuadernador paralelo y LETfuera el encuadernador en serie, los programadores aún lo usarían LETy solo lo sacarían LET*cuando quisieran el encuadernado paralelo. Esto probablemente lo haría LET*raro.
Kaz

de hecho, CLHS especifica el orden de evaluación de los formularios Let 's init .
Will Ness

4
(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.


2

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".


1

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 :)


1

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)

Sí, así se definen. Pero, ¿cuándo necesitarías el primero? En realidad, no estás escribiendo un programa que necesite cambiar el valor de pi en un orden particular, supongo. :-)
Ken

1

El letoperador 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, letes más primitivo, mientras que let*es un azúcar sintáctico para escribir una cascada de let-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 letmás let*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 lete let*intercambiaran sus significados, probablemente sería más raro de usar let*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 la let*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 solo let*corresponde a múltiples anidadas let-s, lo que hace que letse parecen más primitiva, no hay razón para realmente expandir let*en letcompilarlo, e incluso una mala idea.

Una cosa más: tenga en cuenta que Common Lisp en lambdarealidad usa let*semántica similar a la Ejemplo:

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

aquí, el xformulario init para yaccede al xparámetro anterior , y de manera similar se (+1 y)refiere al yopcional 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 a lambdano 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.


0

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 para let. Tiene que haber un compilador que funcione qué formularios están accediendo a qué y luego convierte todos los independientes en más grandes, combinados let.

(Esto es cierto incluso si let*es solo un operador de macro que emite letformas en cascada ; la optimización se realiza en laslet s en ).

No puede implementar let*como un solo ingenuo let, 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 bestá sombreando al exterior, bpor lo que terminamos sumando 2 a nil. 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 #:g01lugar dea .

Entonces, básicamente, let*es la construcción complicada la que debe optimizarse bien para funcionar tan bien como leten los casos en que podría reducirse alet .

Eso por sí solo no justificaría que favorece letmás let*. Supongamos que tenemos un buen compilador; ¿por qué no usar let*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 letversus let*, porque let*claramente no es una abstracción de nivel superior en relación con let. Son de "nivel igual". Con let*, puede introducir un error que se resuelve simplemente cambiando a let. Y viceversa . let*realmente es solo un azúcar sintáctico suave para el letanidamiento que colapsa visualmente , y no una nueva abstracción significativa.


-1

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.


-1

¿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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.