coding style - Paréntesis Lisp
coding-style (13)
¡Además del hecho de que la mayoría de los IDE de Lisp utilizan la comparación de paréntesis, la cantidad de espacio que usaría para escribir cualquier programa razonable sería ridícula! Usted obtendría túnel carpiano de todo el desplazamiento. Un programa de 1.000 líneas estaría cerca de un millón de líneas de código si coloca todos los paréntesis de cierre en su propia línea. Puede parecer más bonito y ser más fácil de leer en un programa pequeño, pero esa idea no se escalaría bien en absoluto.
¿Por qué Lispers formatea su código como se muestra en la muestra 1 en lugar de como se muestra en la muestra 2? Para mí (y supongo, para la mayoría de los demás provenientes de diferentes entornos de programación que Lisp), el formato mostrado en la muestra 2 sería más fácil de leer. ¿Hay alguna razón en particular por la que los Lispers prefieren el estilo de muestra 1?
Muestra 1
(defun factorial (n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))
Muestra 2
(defun factorial (n)
(if (<= n 1)
1
(* n (factorial (- n 1)))
)
)
¿Por qué los programadores de C escriben cosas como:
((a && b) || (c && d))
en lugar de
((a && b) || (c && d
)
)
¿No sería más fácil leer el # 2? :)
En serio, sin embargo, el estilo de cierre agrupado funciona bien también para otros idiomas.
#include <stdlib.h>
#include <stdio.h>
int main(void)
{ int i, j, k;
for (i = 0; i < 1000; i++)
{ for (j = 0; j < 1000; j++)
{ for (k = 0; k < 1000; k++)
{ if (i * i + j * j == k * k)
{ printf("%d, %d, %d/n", i, j, k); } } } }
return 0; }
Después de un tiempo, no se "ven" las llaves, solo la estructura dada por la muesca. Si / más se ve así:
if (cond)
{ stmt;
stmt; }
else
{ stmt; }
cambiar:
switch (expr)
{ case ''3'':
stmt;
break;
case ''4'':
{ stmt;
break; } }
estructuras:
struct foo
{ int x;
struct bar
{ int y; };
union u
{ int a, b;
char c; }; };
Después de un tiempo con Lisp, ya no notará los paréntesis, por lo que su ejemplo dos parece tener un montón de espacios en blanco innecesarios al final. Más específicamente, la mayoría de los usuarios utilizan un editor que conoce los paréntesis y se encarga de cerrar los formularios correctamente, de modo que usted, como desarrollador, no necesita hacer coincidir los paréntesis de apertura y cierre como lo hace en C. por ejemplo.
En cuanto a si la última forma debe ser
(* n (factorial (- n 1)))
o
(* n
(factorial (- n 1)))
eso se reduce principalmente a las preferencias personales y la cantidad de cosas que están sucediendo en el código (en este caso, preferiría lo primero solo porque el código está sucediendo muy poco).
Experimenté el mismo dilema en PHP usando el marco CakePHP y sus muchas estructuras de array()
anidadas, a veces con más de 5 capas de profundidad. Poco después me di cuenta de que ser TOC sobre el paréntesis de cierre no hizo más que perder mi precioso tiempo de desarrollo y me distrajo del objetivo final. La sangría debía ser el aspecto más útil del formato.
Los entornos LISP IDE tienden a equilibrar los paréntesis automáticamente y administrar las sangrías según el nivel de anidamiento. La muestra 2 no trae ninguna ventaja en esas situaciones.
En la herencia de C / FORTRAN / Pascal, se tiende a enfatizar la secuencia sobre el anidamiento (los árboles de código de análisis son menos profundos y más anchos). El final del alcance es un evento más importante en su código: por lo tanto, el énfasis ha sido y hasta cierto punto es más importante.
Para usuarios experimentados de Lisp, el nivel de anidamiento es más importante que encontrar paréntesis de cierre. Poner los paréntesis de cierre en sus propias líneas no ayuda realmente con los niveles de anidación.
La idea básica es que los paréntesis están directamente ALREDEDOR de sus contenidos.
(a)
y no
(a
)
Lo que sigue es esto:
(defun foo (bar)
(foo (bar (baz
...
)
...
)
...
)
)
contra
(defun foo (bar)
(foo (bar (baz ...) ...) ...))
Una de las ideas básicas al editar texto Lisp es que puede seleccionar una lista haciendo doble clic en los paréntesis (o utilizando un comando de teclado cuando el cursor está dentro de la expresión o en los paréntesis). Luego puede cortar / copiar la expresión y pegarla en otra posición en alguna otra función. El siguiente paso es seleccionar la otra función y volver a sangrar la función. Hecho. No es necesario eliminar o introducir nuevas líneas para cerrar paréntesis. Solo pega y vuelve a sangrar. Simplemente encaja. De lo contrario, perdería tiempo formando el código o tendría que volver a formatear el código con alguna herramienta de edición (de modo que los paréntesis de cierre estén en sus propias líneas. La mayoría de las veces eso creará trabajo adicional y dificulta el movimiento de código alrededor.
Hay una ocasión en la que los Lispers experimentados escribirían algún paréntesis de cierre en su propia línea:
(defvar *persons*
(list (make-person "fred")
(make-person "jane")
(make-person "susan")
))
Aquí indica que se pueden agregar nuevas personas. Coloque el cursor directamente antes del segundo paréntesis de cierre en la última línea, presione co (línea abierta), agregue la cláusula e indente los paréntesis para alinearlos nuevamente. Esto guarda el ''problema'' para encontrar los paréntesis correctos y luego presiona regresar, cuando todos los paréntesis están cerrados en una línea.
Parece que esta no es una pregunta real, solo un juicio sobre Lisp, pero la respuesta es perfectamente obvia (¡pero no, aparentemente, para los que no son de Lisper!): Los paréntesis en Lisp no son en absoluto equivalentes a llaves en lenguajes similares a C - - más bien, son precisamente equivalentes a paréntesis en lenguajes tipo C. Lispers generalmente no escriben
(foo bar baz
)
exactamente por la misma razón por la que los programadores C no escriben
foo(bar, baz
);
La razón
(if foo
(bar)
(baz))
se ve diferente de
if (foo) {
bar();
} else {
baz();
}
Tiene más que ver con el if
que los paréntesis!
Podría considerar las variantes de Lisp que prescinden de (). En Genyris se ve así:
def factorial (n)
if (< n 2) 1
* n
factorial (- n 1)
Porque los programadores de Lisp miran la sangría y la "forma", no los paréntesis.
Porque no hay ninguna necesidad de alinear los parens de cierre. No añade nada semánticamente. Para saber cuántos cerrar, la mayoría de los Lispers utilizan un buen editor, como Emacs, que combina parens con parens de cierre y, por lo tanto, hace que la tarea sea trivial.
En Python, no hay parentes de cierre ni palabras clave end
, y los Pythonistas viven bien.
Salvar todo el espacio parece ser la razón principal. Si es casi tan legible (especialmente una vez que estás acostumbrado a la sintaxis) por qué usar las 3 líneas adicionales.
Solo la primera y obvia razón: 4 LOC en el primer caso vs. 7 LOC en el segundo.
Cualquier editor de texto que tenga conocimiento de la sintaxis de LISP resaltará los paréntesis coincidentes / no coincidentes, por lo que no es el problema.
Tengo una explicación en cuanto a que los programadores de C tienen un problema con Lispers poniendo todos los paréntesis de cierre restantes al final. La práctica me parece que el significado de paréntesis podría ser la razón. Esta explicación se superpone y se expande en algunas otras respuestas.
Para un programador de C (u otro lenguaje que use {llaves} como C), parece que Lisp usa # | comentarios | # en lugar de / * comentarios * /. Y parece que los paréntesis Lisp () están en lugar de corchetes {}.
Pero realmente, los paréntesis Lisp significan muy cerca de lo mismo que en C (y lenguajes similares que usan paréntesis). Considerar
defun f () nil
Esto define una función f que no hace absolutamente nada, nil. Y escribo esa línea exactamente como está en el Oyente de Lisp, y simplemente responde "F". En otras palabras, lo acepta, sin un paréntesis de principio y fin alrededor de todo el asunto.
Lo que creo que sucede cuando escribes eso en el Oyente es que el Oyente pasa lo que escribiste a Eval. Pasarlo a Eval podría representarse como:
Eval( Listener() )
Y el oyente acepta mi entrada y devuelve lo que escribí. Y consideraría "Listener ()" reemplazado en la expresión para que se convierta en
Eval (defun f () nil)
REPL (read, eval, print, loop) podría representarse conceptualmente como una recursión
/* REPL expressed in C */
Loop()
{
Print ( Eval ( Listen () ) );
Loop();
}
En el ejemplo se entiende que Listen (), Eval () y Print () se definen en otra parte o están incorporados en el idioma.
El oyente me deja escribir
setf a 5
y también me permite escribir
(setf a 5)
pero se queja cuando escribo
((setf a 5))
Si los paréntesis fueran equivalentes a las llaves del lenguaje C, entonces el oyente lo aceptaría. En mis compiladores C / C ++, puedo escribir
void a_fun(void)
{{
}}
sin queja Incluso puedo escribir
void a_fun(void)
{{{
}}}
sin queja del compilador C / C ++.
Pero si me atrevo a escribir
void a_fun((void))
{
}
mi compilador de C / C ++ se queja - ¡tal como se queja el oyente de Lisp!
Para un programador de C que escribe llamadas de función anidadas, parece que es más natural escribir
fun_d( fun_c( fun_b( fun_a())));
que escribir
fun_d( fun_c( fun_b( fun_a()
)
)
);
En C, las llaves demarcan un bloque de código. Y un bloque de código es parte de una definición de función. Los paréntesis Lisp no demarcan bloques. Son algo así como los paréntesis de C. En C, los paréntesis demarcan una lista; tal vez una lista de parámetros pasados a una función.
En Lisp, aparecen los paréntesis de apertura "(" significa que vamos a comenzar una nueva lista, a la que apuntará el lado CAR de una construcción CONS. Luego enumeramos los elementos que se incluirán o se señalarán opcionalmente por el lado CAR del CONS, con el lado de CDR apuntando a nil (para el final de la lista) o al siguiente CONS. El paréntesis de cierre ")" significa terminar la lista con un nil, y volver para continuar con el nivel anterior de la lista. Por lo tanto, el lenguaje C no hace CONS. Por lo tanto, hay una pequeña diferencia entre los paréntesis de C y los de Lisp. Aun así, parece muy cercano, tal vez incluso prácticamente lo mismo.
Algunos han escrito que Lisp se parece más a C si mueve la función fuera de los paréntesis. Por ejemplo,
(setf a 5)
se convierte en
setf(a 5)
Pero lo que realmente es, es que Eval requiere que el primer elemento de una lista sea la función a la que debe llamar Eval. Es más como pasar una función de C un puntero a una función. Entonces, lo que realmente es es
eval(setf a 5)
Y eso se parece más a C. Incluso puedes escribirlo en el oyente de la misma manera sin quejas del oyente o eval. Solo está diciendo que eval debería llamar a eval, que luego debería llamar a setf. El resto de la lista son parámetros para setf.
De todos modos, eso es justo lo que creo que veo.
Es solo que Eval necesita saber a qué procesador llamar.
Y creo que esta explicación proporciona una comprensión de por qué los programadores de C piensan inicialmente que los Lispers deberían alinear los paréntesis de cierre entre los paréntesis de apertura que cierran. El programador de C piensa erróneamente que el paréntesis de Lisp corresponde a las llaves de C. Ellos no Los paréntesis de Lisp corresponden a los paréntesis de C.