¿Por qué la comunidad de Lisp prefiere acumular todos los paréntesis al final de la función?

26

¿Por qué la comunidad de Lisp prefiere acumular todos los paréntesis al final de la función?

(defn defer-expensive [cheap expensive]
  (if-let [good-enough (force cheap)]
    good-enough
    (force expensive)))

¿Por qué no emplear una convención como C o Java?
Bien, Lisp es mucho más antiguo que esos idiomas, pero estoy hablando de los Lispers contemporáneos.

(defn defer-expensive [cheap expensive]
  (if-let [good-enough (force cheap)]
    good-enough
    (force expensive)
  )
)

Nota: El fragmento de código es del libro "The Joy of Clojure".

Quirón
fuente
13
Me dijeron que en los días de las tarjetas perforadas, era una práctica común terminar un programa Lisp con una tarjeta adicional con 80 paréntesis correctos, solo para asegurarse de que hubiera suficientes para coincidir con todos los paréntesis abiertos en el programa .
Alger
2
Wow me gusta. Siempre he odiado los paréntesis, pero su segundo ejemplo me parece agradable. No es tan bueno como Python, pero mejora.
Eric Wilson
55
¿porque ellos pueden?
Muad'Dib
Sería bueno si un dialecto de Clojure promoviera la sangría a ser estructura de código. Eso eliminaría el paréntesis por completo (como F #, etc.). Por supuesto, el paréntesis seguiría siendo legal, si se desea.
intrepidis

Respuestas:

28

Una de las razones por las cuales los lenguajes basados ​​en Algol fomentan las llaves en su propia línea es para alentar la adición de más líneas entre las llaves delimitadoras sin tener que mover las llaves. Es decir, si uno comienza con

if (pred)
{
  printf("yes");
}

es fácil venir y agregar otra declaración entre llaves:

if (pred)
{
  printf("yes");
  ++yes_votes;
}

Si la forma original hubiera sido

if (pred)
{ printf("yes"); }

entonces tendríamos que haber "movido" dos llaves, pero mi ejemplo está más relacionado con la última. Aquí, las llaves delimitan lo que pretende ser una secuencia de declaraciones , en su mayoría invocadas para efectos secundarios.

Por el contrario, Lisp carece de declaraciones; cada forma es expresión , dando algún valor, incluso si en algunos casos raros (pensando en Common Lisp), ese valor se elige deliberadamente para que sea "sin valores" a través de una (values)forma vacía . Es menos común encontrar secuencias de expresiones , a diferencia de las expresiones anidadas . El deseo de "abrir una secuencia de pasos hasta el delimitador de cierre" no surge con tanta frecuencia, porque a medida que las declaraciones desaparecen y los valores devueltos se vuelven más comunes, es más raro ignorar el valor devuelto de una expresión, y por lo tanto más Es raro evaluar una secuencia de expresiones para el efecto secundario solo.

En Common Lisp, el prognformulario es una excepción (al igual que sus hermanos):

(progn
  (exp-ignored-return-1)
  (exp-ignored-return-2)
  (exp-taken-return))

Aquí, prognevalúa las tres expresiones en orden, pero descarta los valores de retorno de las dos primeras. Podría imaginarse escribiendo el último paréntesis de cierre en su propia línea, pero tenga en cuenta nuevamente que, dado que la última forma es especial aquí ( aunque no en el sentido de Common Lisp de ser especial ), con un tratamiento distinto, es más probable que se agreguen nuevos expresiones en el medio de la secuencia, en lugar de simplemente "agregar otra al final", ya que las personas que llaman no se verán afectadas por ningún efecto secundario nuevo sino por un posible cambio en el valor de retorno.

Haciendo una simplificación general, los paréntesis en la mayoría de las partes de un programa Lisp delimitan argumentos pasados ​​a funciones, al igual que en lenguajes tipo C, y no delimitan bloques de instrucciones. Por las mismas razones, tendemos a mantener los paréntesis que delimitan una llamada de función en C cerca de los argumentos, así que también hacemos lo mismo en Lisp, con menos motivación para desviarse de esa agrupación cercana.

El cierre de los paréntesis es mucho menos importante que la sangría del formulario donde se abren. Con el tiempo, uno aprende a ignorar los paréntesis y a escribir y leer por forma, al igual que los programadores de Python. Sin embargo, no dejes que esa analogía te lleve a pensar que valdría la pena eliminar los paréntesis por completo. No, ese es el debate mejor guardado comp.lang.lisp.

seh
fuente
2
Creo que agregar al final no es tan raro, por ejemplo (let ((var1 expr1) more-bindings-to-be-added) ...), o(list element1 more-elements-to-be-added)
Alexey
14

Porque no ayuda Usamos sangría para mostrar la estructura del código. Si queremos separar bloques de código, usamos líneas realmente vacías.

Como la sintaxis de Lisp es tan consistente, los paréntesis son la guía definitiva para la sangría tanto para el programador como para el editor.

(Para mí, la pregunta es más bien por qué a los programadores de C y Java les gusta tirar sus llaves).

Solo para demostrar, asumiendo que estos operadores estaban disponibles en un lenguaje tipo C:

Foo defer_expensive (Thunk cheap, Thunk expensive) {
    if (Foo good_enough = force (cheap)) {
        return good_enough; }
    else {
        return force (expensive); }}

Dos llaves de cierre cierran dos niveles de anidación. La sintaxis es obviamente correcta. En analogía con la sintaxis de Python, las llaves son solo tokens explícitos INDENT y DEDENT.

Por supuesto, este podría no ser el TM de "un verdadero estilo de aparato ortopédico" , pero creo que es solo un accidente histórico y un hábito.

Svante
fuente
2
Además, casi todos los editores Lisp marcan el paréntesis correspondiente al cerrar (algunos también correcta )a ]o viceversa), para que sepa que cerró la cantidad correcta, incluso si usted no comprueba los niveles de sangría.
Configurador
66
WRT "la pregunta", porque "tirar" tokens de cierre al estilo del segundo ejemplo te permite alinearlos fácilmente con tus ojos y ver qué cierra qué, incluso si solo estás en un editor de texto sin coincidencia automática / Destacando características.
Mason Wheeler
1
@Mason Wheeler Sí, exactamente.
Chiron
3
TBH, lo que esto me dice es que los parens en LISP son en su mayoría redundantes, con la posibilidad (como con C ++) de que la sangría pueda inducir a error; si la sangría le dice lo que necesita saber, también debería decirle al compilador lo mismo, como en Haskell y Python. Por cierto, tener las llaves en el mismo nivel de sangría que if / switch / while / whatever en C ++ ayuda a prevenir casos en los que la sangría sea engañosa: un escaneo visual en el LHS le dice que cada llave abierta coincide con una llave cerrada y esa sangría es consistente con esas llaves.
Steve314
1
@ Steve314: No, la sangría es redundante. La sangría también puede inducir a error en Python y Haskell (piense en la sangría o los tabulares únicos), es solo que el analizador también es engañoso. Creo que los paréntesis son una herramienta para el escritor y el analizador, mientras que la sangría es una herramienta para el escritor y el lector (humano). En otras palabras, la sangría es un comentario, los paréntesis son sintaxis.
Svante
11

El código es mucho más compacto entonces. El movimiento en el editor es por expresiones s de todos modos, por lo que no necesita ese espacio para editar. El código se lee principalmente por estructura y oraciones, no siguiendo delimitadores.

Rainer Joswig
fuente
Qué honor recibir una respuesta de usted :) Gracias.
Quirón
¿Qué editor se mueve por expresiones s? Más específicamente, ¿cómo puedo vimhacer eso?
Hasen
2
@ HasenJ Puede que necesites el vimacs.vim complemento . : P
Mark C
5

Lispers, ya saben, odio bletcherous ya que puede ser, no es cierto no sé qué con el segundo ejemplo:

(defn defer-expensive [cheap expensive]
  (if-let [good-enough (force cheap)]
    good-enough
    (force expensive)
  )
)

Al principio no pude señalar el origen del encanto, por así decirlo, y luego me di cuenta de que solo faltaba un gatillo:

(defn defer-expensive [cheap expensive]      
  (if-let [good-enough (force cheap)]
    good-enough   ;   )
    (force expensive) 
  )
)

Voilà!

¡Voy a practicar mi agarre de gángster Lisp ahora!

Kaz
fuente
2
Esta es una de las respuestas más divertidas y subestimadas que he visto en este sitio. Me vuelvo el sombrero.
byxor
0

En mi caso, encuentro que las líneas dedicadas a delimitadores son una pérdida de espacio en la pantalla, y cuando escribes código en C también existe el estilo de

if (pred) {
   printf("yes");
   ++yes_votes;
}

¿Por qué las personas colocan la llave de apertura en la misma línea del "if" para ahorrar espacio, y porque parece redundante tener su propia línea cuando el "if" ya tiene su propia línea?

Alcanza el mismo resultado al juntar el paréntesis al final. En C se vería raro porque las declaraciones terminan en punto y coma y el par de paréntesis no se abre así

{if (pred)
    printf("yes");
}

Es como ese cierre de llave al final, parece fuera de lugar. se abre así

if (pred) {
    printf("yes");
}

dándole una visión clara del bloque y sus límites mediante '{' & '}'

Y con la ayuda de un editor que coincide con los paréntesis al resaltarlos como lo hace vim, puede ir al final donde se empaquetan todos los paréntesis y moverse a través de ellos fácilmente y hacer coincidir todos los paréntesis de apertura y ver el formulario lisp anidado

(defn defer-expensive [cheap expensive]
    _(if-let [good-enough (force cheap)]
    good-enough
    (force expensive)_))

puede colocar el cursor en el par de cierre en el medio y resaltar el par de apertura del if-let.

kisai
fuente