net - LET versus LET*en Common Lisp
lisp descargar (16)
¿quién se siente como volver a escribir letf vs letf * otra vez? la cantidad de llamadas de protección contra desenrollo?
más fácil para optimizar enlaces secuenciales.
tal vez afecta el 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))))) (values ()))
[Creo que funciona] punto es, más simple es más fácil de leer a veces.
Entiendo la diferencia entre LET y LET * (enlace paralelo versus secuencial), y como una 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.
Editar: OK, entiendo por qué un tipo inventó LET *, presumiblemente como una macro, allá por cuando. 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 viste un LOOP con declaraciones de tipo? Es tan difícil entender que nunca los he visto usar.) Antes de los puntos de referencia de Dick Gabriel a fines de la década de 1980, 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 *.
Además de la respuesta de Rainer Joswig , y desde un punto de vista purista o teórico. Let & Let * representa 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 de volver a casa y pensar en un lenguaje funcional puro, a diferencia del lenguaje secuencial en el que paso la mayor parte del día trabajando :)
Bajo 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.
Bajo 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.
Un let*
debe estar bien optimizado para colapsar las extensiones de entorno innecesarias para que sea adecuado como reemplazo diario para let
. Tiene que haber un compilador que funcione qué formularios acceden a qué y luego convierte a todos los independientes en let
más grande y combinado.
(Esto es cierto incluso si let*
es solo un operador de macro que emite formularios let
cascada; la optimización se realiza en aquellos en cascada s).
No se puede implementar let*
como una sola versión ingenua, con asignaciones de variables ocultas para realizar 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 el exterior b
así que terminamos agregando 2 a nil
. Este tipo de transformación se puede hacer si cambiamos el nombre alfa de todas estas variables. El medio ambiente 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
lugar de a
.
Entonces, básicamente, let*
es la construcción complicada que debe optimizarse para funcionar y let
casos en los que podría reducirse.
Eso solo no justificaría favorecer let
over 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 dependemos tanto como sea posible de buenas implementaciones de los constructos de alto nivel, de modo que rara vez tenemos que sacrificar su uso por el bien del rendimiento. Es por eso que estamos trabajando en un lenguaje como Lisp en primer lugar.
Ese razonamiento no se aplica amablemente a let
versus let*
, porque let*
no es claramente una abstracción de nivel superior en relación con let
. Son acerca del "nivel igual". Con let*
, puedes introducir un error que se resuelve simplemente cambiando a let
. Y viceversa let*
realidad es solo un azúcar sintáctico suave para colapsar visualmente let
anide, y no una nueva abstracción significativa.
Con le permite usar encuadernación paralela,
(setq my-pi 3.1415)
(let ((my-pi 3) (old-pi my-pi))
(list my-pi old-pi))
=> (3 3.1415)
Y con el enlace de serie Let *,
(setq my-pi 3.1415)
(let* ((my-pi 3) (old-pi my-pi))
(list my-pi old-pi))
=> (3 3)
El OP pregunta "alguna vez realmente necesito 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, 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 cba) no va a funcionar. Pero esas macros son bastante descuidadas por diversas razones; por lo que socava el argumento de la "necesidad real".
El operador let
introduce un único entorno para todas las vinculaciones 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 que let*
es un azúcar sintáctico para escribir una cascada de let
-s.
Pero no importa eso. (Y voy a dar justificación más adelante a continuación para saber por qué deberíamos "no importa"). El hecho es que hay dos operadores, y en "99%" del código, no importa cuál use. La razón para preferir let
let*
es simplemente que no tiene *
pendiente al final.
Siempre que tenga dos operadores y uno tenga un *
colgando de su nombre, 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
y let*
intercambiaran sus significados, probablemente sería más raro usar let*
que ahora. El comportamiento de encuadernación en serie no molestará a la mayoría del código que no lo requiera: raramente debería usarse let*
para solicitar un comportamiento paralelo (y tales situaciones también podrían corregirse cambiando el nombre de las variables para evitar el sombreado).
Ahora para esa discusión prometida. Aunque let*
introduce conceptualmente múltiples entornos, es muy fácil compilar el constructo let*
para generar un único entorno. Así que aunque (al menos si ignoramos las declaraciones ANSI CL), existe una igualdad algebraica de que una única let*
corresponde a múltiples let
anidados, lo que hace let
parezca más primitivo, no hay razón para expandir let*
into let
compilarlo, e incluso una mala idea.
Una cosa más: ¡tenga en cuenta que la lambda
Common Lisp realmente usa la semántica let*
-like! Ejemplo:
(lambda (x &optional (y x) (z (+1 y)) ...)
aquí, la x
init-form para y
accede al parámetro x
anterior, y de manera similar (+1 y)
refiere a la anterior y
opcional. En esta área del lenguaje, se muestra una clara preferencia por la visibilidad secuencial en el enlace. Sería menos útil si las formas en una lambda
no pudieran ver los parámetros; un parámetro opcional no puede ser sucintamente predeterminado en términos del valor de los parámetros previos.
En LISP, a menudo existe el deseo de utilizar los constructos más débiles posibles. Algunas guías de estilo le indicarán que use =
lugar de eql
cuando sabe 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 de eficiencia al decir solo lo que quiere decir, y no utilizar construcciones más fuertes. Si tiene inicializaciones con LET
, pueden ejecutarse 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.
Hace poco escribí una función de dos argumentos, donde el algoritmo se expresa más claramente si sabemos cuál 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 que b
fuera más grande, si hubiéramos usado let*
, accidentalmente hubiéramos establecido b
con el mismo valor.
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 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, 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 de inicio se evalúa y luego se une al símbolo en secuencia, de izquierda a derecha.
No necesita LET, pero normalmente lo quiere .
LET sugiere que solo estás haciendo un enlace 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 que LET * (según el compilador, optimizador, etc.):
- los enlaces paralelos se pueden ejecutar en paralelo (pero no sé si algún sistema LISP realmente lo hace, y los formularios init aún se deben ejecutar de forma secuencial)
- los 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 una búsqueda de variables más rápida .
(Las viñetas anteriores se aplican a Scheme, otro dialecto de LISP. Clisp puede diferir).
Presumiblemente, al usar 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))
Uso mayoritariamente LET, a menos que específicamente necesite LET *, pero a veces escribo código que necesita LET de forma explícita, generalmente cuando se realiza una clasificación por defecto (generalmente complicada). Desafortunadamente, no tengo ningún ejemplo de código práctico a mano.
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)))
voy un paso más allá y utilizo bind que unifica let
, let*
, multiple-value-bind
, destructuring-bind
, etc., e incluso es extensible.
en general, me gusta utilizar el "constructo más débil", pero no con " let
y amigos" porque solo hacen ruido al código (¡advertencia de subjetividad! No hay necesidad de intentar convencerme de lo contrario ...)
LET
no es realmente un primitivo en un lenguaje de programación funcional , ya que puede reemplazarse 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 simple. LET
Y NO LET*
.
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 descendente / izquierda-derecha de ''efectos'' (rebindings). El uso de señales LET*
para el programador (el que lee el código) indica que los enlaces no son independientes, pero existe 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 computan de izquierda a derecha. Exactamente cómo se evalúan los valores de 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 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))
Arriba declara que la variable i
es 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 utilizaron con Lisp no CL. Common Lisp en sí era nuevo en 1985: el libro CLtL1 que describía el lenguaje acababa de ser publicado 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
frente a LET*
la principal diferencia es que el código que usa LET
es más fácil de entender para los humanos, ya que las cláusulas vinculantes son independientes entre sí, especialmente porque es un mal estilo aprovechar la evaluación de izquierda a derecha (no establecer variables como un efecto secundario).
(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 la idea lo que cuenta.