2009-02-16 24 views
78

Entiendo la diferencia entre LET y LET * (enlace paralelo versus secuencial), y como cuestión teórica tiene perfecto sentido. Pero, ¿hay algún caso en el que alguna vez hayas necesitado LET? En todo mi código Lisp que he visto recientemente, puede reemplazar cada LET con LET * sin cambios.LET versus LET * en Common Lisp

Edit: OK, entiendo por qué algún tipo inventó LET *, presumiblemente como una macro, hace mucho tiempo. Mi pregunta es, dado que LET * existe, ¿hay alguna razón para que LET permanezca? ¿Has escrito algún código Lisp real donde un LET * no funcione tan bien como un LET simple?

No compro el argumento de la eficiencia. En primer lugar, reconocer los casos donde LET * se puede compilar en algo tan eficiente como LET simplemente no parece tan difícil. En segundo lugar, hay muchas cosas en las especificaciones de CL que simplemente no parecen haber sido diseñadas en términos de eficiencia en absoluto. (¿Cuándo fue la última vez que vio un LOOP con declaraciones de tipo? Es tan difícil imaginar 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. Cuál fue mi corazonada, pero es reconfortante saber que nadie tiene un caso estúpidamente simple que me faltaba donde DEJAR hizo un montón de cosas ridículamente más fácil que LET *.

+0

paralelo es una mala elección de palabras; solo enlaces anteriores son visibles. la unión paralela se parecería más a las uniones "... donde ..." de Haskell. – jrockway

+0

No intenté confundir; Creo que esas son las palabras usadas por la especificación. :-) – Ken

+10

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

Respuesta

9

En LISP, a menudo existe el deseo de utilizar las construcciones más débiles posibles. Algunas guías de estilo le indicarán que use = en lugar de eql cuando sepa que los elementos comparados son numéricos, por ejemplo. La idea a menudo es especificar lo que quiere decir en lugar de programar la computadora de manera eficiente.

Sin embargo, puede haber mejoras reales en la eficiencia al decir solo lo que quiere decir, y no utilizar constructos más fuertes. Si tiene inicializaciones con LET, se pueden ejecutar en paralelo, mientras que las inicializaciones LET* deben ejecutarse secuencialmente. No sé si algunas implementaciones realmente harán eso, pero algunas podrían hacerlo en el futuro.

+2

Buen punto. Aunque dado que Lisp es un lenguaje de alto nivel, eso me hace preguntarme por qué "constructos más débiles posibles" es un estilo tan deseado en Lisp. 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 en cierta medida por personas (como yo) a las que les gusta utilizar 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. –

+0

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

4

Presumiblemente al utilizar let, el compilador tiene más flexibilidad para reordenar el código, quizás para mejorar el espacio o la velocidad.

Estilísticamente, el uso de enlaces paralelos muestra la intención de que los enlaces estén agrupados; 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)) 
+0

En el 90% del código, no importa si usa 'LET' o' LET * '. Entonces, si usas '*', estás agregando un glifo innecesario. Si 'LET *' era el encuadernador paralelo y 'LET' eran el encuadernador en serie, los programadores seguirían usando' LET', y solo sacarían 'LET *' cuando quisieran encuadernación paralela. Esto probablemente haría 'LET *' raro. – Kaz

21

Vengo llevando ejemplos artificiales. Comparar el resultado de esto:

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

con el resultado de ejecutar este:

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

¿Cuida desarrollar por qué es este el caso? –

+3

@John: en el primer ejemplo, la vinculación de 'a' se refiere al valor externo de' c'. En el segundo ejemplo, donde 'let *' permite que los enlaces se refieran a enlaces anteriores, el enlace 'a' se refiere al valor interno de' c'. Logan no miente sobre 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, la unión de 'a' debe estar un espacio encima, para alinearse con' c''s, y el 'cuerpo' del 'let' interno debe estar a solo dos espacios del' let'. – zem

+2

Esta respuesta proporciona una idea importante. Uno ** usa ** específicamente 'let' cuando uno quiere _avoid_ tener enlaces secundarios (con lo que simplemente quiero decir que no es el primero) se refiere al primer enlace, pero _do_ desea sombrear un enlace anterior - usando el anterior valor de ello para inicializar uno de sus enlaces secundarios. – lindes

33

Usted no necesidad LET, pero que normalmente quiere ella.

LET sugiere que solo está haciendo un encuadernado en paralelo estándar sin nada complicado. LET * induce restricciones en el compilador y sugiere al usuario que hay una razón por la cual 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 de dejar * (dependiendo del compilador, optimizador, etc.):

  • fijaciones paralelas se pueden ejecutar en paralelo (pero no sé si cualquier sistema LISP realmente hace esto, y los formularios init aún se deben ejecutar secuencialmente)
  • enlaces paralelos crean un único entorno nuevo (alcance) para todas las vinculaciones. Los enlaces secuenciales crean un nuevo entorno anidado para cada enlace individual. Los enlaces paralelos usan menos memoria y tienen consulta de variable más rápida.

(Los puntos anteriores se aplican a Scheme, otro dialecto LISP. Clisp puede diferir.)

+1

Nota: Consulte [esta respuesta] (http://stackoverflow.com/a/562975/313756) (y/o la sección vinculada de hiperespec) para obtener un poco de explicación sobre por qué su primer punto de polla es - engañosa, vamos a decir. Los _ enlaces_ ocurren en paralelo, pero _forms_ se ejecutan secuencialmente, según la especificación. – lindes

+4

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

+1

La diferencia no solo es importante para el compilador. Uso let y let * como pistas de lo que está sucediendo. Cuando veo entrar mi código, sé que los enlaces son independientes, y cuando veo let *, sé que los enlaces dependen el uno del otro. Pero solo lo sé porque me aseguro de usar let y let * constantemente. – Jeremiah

7

i ir un paso más allá y uso bind que unifica let, let *, múltiple-valor-bind, desestructuración -bind, etc, y es incluso extensible.

general me gusta el uso de la "construcción más débil", pero no con dejo & amigos, ya que sólo dan el ruido al código (advertencia subjetividad! Hay necesidad de intentar convencerme de lo contrario ...)

+0

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

8

El La diferencia principal 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 init se ejecuten en paralelo ni permite cambiar el orden de los formularios init. 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, los init-forms se evalúan primero, de izquierda a derecha, luego se crean los enlaces, de izquierda a derecha. En LET *, la forma de inicio se evalúa y luego se une al símbolo en secuencia, de izquierda a derecha.

CLHS: Special Operator LET, LET*

+1

Parece que esta respuesta quizás está sacando algo de energía de ser una respuesta a [esta respuesta] (http://stackoverflow.com/a/555136/313756)? Además, según la especificación vinculada, se dice que _bindings_ se hace en paralelo en 'LET', aunque es correcto avisar que los formularios init se ejecutan en serie. Si eso tiene alguna diferencia práctica en cualquier implementación existente, no lo sé. – lindes

-1

sobre todo me usar le permiten, a menos que yo necesito specifgically LET *, pero a veces escribir código que explícitamente necesidades LET, por lo general cuando se hace surtidos (generalmente complicado) morosos. Desafortunadamente, no tengo ningún ejemplo de código práctico a mano.

74

LET sí mismo no es un verdadero primitiva en un lenguaje de programación funcional , ya que puede reemplazado por LAMBDA. De esta manera:

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

es similar 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 similar a

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

Se puede imaginar que es lo más sencillo. LET y no LET*.

LET hace que la comprensión del código sea más fácil. Uno ve un montón de enlaces y uno puede leer cada enlace individualmente sin la necesidad de comprender el flujo descendente/izquierda-derecha de 'efectos' (rebindings).El uso de 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 descendente, lo que complica las cosas.

Common Lisp tiene la regla de que los valores para los enlaces en LET se calculan de izquierda a derecha. Exactamente cómo se evalúan los valores de una llamada de función, de izquierda a derecha. Por lo tanto, LET es la declaración conceptualmente más simple y debe usarse de forma predeterminada.

Tipos en LOOP? Se usan con bastante frecuencia. Hay algunas formas primitivas de declaración de tipo que son fáciles de recordar. Ejemplo:

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

declara Por encima de la variable i ser un 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 usaron con los Lis Lis no CL. Common Lisp en sí era nuevo en 1985 - el libro CLtL1 que describía el lenguaje que acababa de publicarse 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 código usando LET es más fácil de entender para los humanos, puesto que las cláusulas de unión son independientes entre sí - sobre todo porque es un mal estilo de tomar ventaja de la izquierda a la derecha de evaluación (no estableciendo variables como un efecto secundario).

+3

¡No, no! Lambda no es una primitiva real porque puede reemplazarse con LET, y una lambda de nivel inferior que solo proporciona una API para llegar a los valores del argumento: '(low-level-lambda 2 (let ((x (car% args%))) (y (cadr args))) ...) ':) – Kaz

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 la intención es lo que cuenta.

1

Además de la respuesta Rainer Joswig's, y desde un punto de vista purista o teórico. Deje & Deje * 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á quitando la diversión de volver a casa y pensar en un lenguaje funcional puro, en lugar del lenguaje secuencial donde paso la mayor parte del día :) trabajar con

1

con permitirá utilizar paralelamente unión,

(setq my-pi 3.1415) 

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

Y con Let * serie de unión,

(setq my-pi 3.1415) 

(let* ((my-pi 3) (old-pi my-pi)) 
    (list my-pi old-pi)) 
=> (3 3) 
+0

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

-1

que se siente como volver a escribir letf vs letf * otra vez? la cantidad de llamadas de protección contra desenrollo?

más fácil de optimizar los enlaces secuenciales.

tal vez afecta al env?

permite continuar con la extensión dinámica?

veces (dejar (xyz) (setq z 0 y 1 x (+ (setq x 1) (prog1 (+ xy) (setq x (1- x))))) (valores()))

[Creo que funciona] punto es, más simple es más fácil de leer a veces.

8

Hace poco escribí una función de dos argumentos, donde el algoritmo se expresa más claramente 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 utilizado let*, tendríamos accidentalmente en a y b en el mismo valor.

+1

A menos que necesite los valores de xey más adelante dentro del 'let' externo, esto se puede hacer más simple (y claramente) con:' (rotatef xy) '- no es una mala idea, pero aún así parece un tramo. – Ken

+0

Eso es verdad. Puede ser más útil si xey son variables especiales. –

0

El operador let presenta un único entorno para todas las vinculaciones que especifica. let*, al menos conceptualmente (y si hacemos caso de las declaraciones por un momento) presenta múltiples entornos:

Es decir:

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

es como:

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

Por lo tanto, en cierto sentido, let es más primitivo, mientras que let* es un azúcar sintáctico para escribir una cascada de let -s.

Pero no importa eso. El hecho es que hay dos operadores, y en "99%" del código, no importa cuál use. La razón para preferir let sobre let* es simplemente que no tiene el * colgando al final.

Siempre que tenga dos operadores, y uno tenga un * colgando de su nombre, utilice el que no tiene el * 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 y let* intercambiaron sus significados, probablemente sería más raro de usar let* que ahora. El comportamiento de encuadernación en serie no molestará a la mayoría del código que no lo requiera: rara vez se debe usar el let* para solicitar un comportamiento paralelo (y tales situaciones también podrían corregirse cambiando el nombre de las variables para evitar el sombreado).

2

El OP pregunta "alguna vez realmente necesario LET"?

Cuando se creó Common Lisp había una carga de barco de código Lisp existente en dialectos surtidos. El resumen que aceptaron las personas que diseñaron Common Lisp fue crear un dialecto de Lisp que proporcionaría un terreno común. Ellos "necesitaban" hacer más fácil y atractivo el código de puerto existente en Common Lisp. Dejar LET o LET * fuera del idioma podría haber servido para otras virtudes, pero habría ignorado ese objetivo clave.

Uso LET con preferencia a 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 *, sabrá que los valores enlazados anticipadamente se usarán en un enlace posterior.¿"Necesito" hacer eso, no? pero creo que es útil. Dicho esto, he leído, rara vez, el código predeterminado en LET * y la aparición de señales LET que el autor realmente quería. Es decir. por ejemplo para cambiar 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 c b a) no va a funcionar. Pero esas macros son bastante descuidadas por diversas razones; por lo que socava el argumento de la "necesidad real".

0

En let, todas las expresiones de inicialización variable ven exactamente el mismo entorno léxico: el que rodea al let. Si esas expresiones capturan cierres léxicos, todos 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* deben estar bien optimizada para colapsar las extensiones de entorno innecesarios a fin de adecuado como un reemplazo de todos los días para let. Tiene que haber un compilador que funcione qué formularios acceden a qué y luego convierte todos los independientes en uno más grande, combinado let.

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

No se puede poner en práctica let* como una sola ingenua let, con asignaciones de variables ocultas que hacer las inicializaciones debido a la falta de definición del alcance adecuado será revelada:

(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 el exterior b, así que terminamos agregando 2 a nil. Este tipo de transformación puede hacerse si cambiamos el nombre alfa de todas estas variables. El entorno se aplana muy bien:

(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 tenemos que considerar el soporte de depuración; si el programador entra en este ámbito léxico con un depurador, ¿queremos que se ocupen de #:g01 en lugar de a?

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

Eso solo no justificaría favorecer a let sobre let*. Supongamos que tenemos un buen compilador; ¿Por qué no usar let* todo el tiempo?

Como principio general, deberíamos favorecer construcciones de alto nivel que nos hagan productivos y reducir errores, sobre construcciones de bajo nivel propensas a errores y dependamos tanto en las buenas implementaciones de construcciones de alto nivel para que podamos rara vez tiene que sacrificar su uso por el bien del rendimiento. Es por eso que estamos trabajando en un lenguaje como Lisp en primer lugar.

Este razonamiento no se aplica muy bien a let frente let*, porque let* no es claramente un nivel de abstracción más alto en relación a let. Son acerca del "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 colapso visual let anidando, y no es una nueva abstracción significativa.