Por favor explique algunos de los puntos de Paul Graham en Lisp

146

Necesito ayuda para comprender algunos de los puntos de What Grays Lisp de Paul Graham .

  1. Un nuevo concepto de variables. En Lisp, todas las variables son efectivamente punteros. Los valores son los que tienen tipos, no variables, y asignar o vincular variables significa copiar punteros, no lo que apuntan.

  2. Un tipo de símbolo. Los símbolos difieren de las cadenas en que puede probar la igualdad comparando un puntero.

  3. Una notación para el código usando árboles de símbolos.

  4. Todo el lenguaje siempre disponible. No existe una distinción real entre tiempo de lectura, tiempo de compilación y tiempo de ejecución. Puede compilar o ejecutar código mientras lee, leer o ejecutar código mientras compila, y leer o compilar código en tiempo de ejecución.

¿Qué significan estos puntos? ¿Cómo son diferentes en lenguajes como C o Java? ¿Algún otro idioma que no sea el de la familia Lisp tiene alguna de estas construcciones ahora?

unj2
fuente
10
No estoy seguro de que la etiqueta de programación funcional esté garantizada aquí, ya que es igualmente posible escribir código imperativo u OO en muchos Lisps como lo es escribir código funcional, y de hecho hay una gran cantidad de Lisp no funcional código alrededor. Sugeriría que elimine la etiqueta fp y agregue clojure en su lugar; con suerte, esto podría traer alguna entrada interesante de Lispers basados ​​en JVM.
Michał Marczyk
58
¡También tenemos una paul-grahametiqueta aquí! Genial ...
missingfaktor
@missingfaktor Quizás necesite una solicitud de burninate
cat

Respuestas:

98

La explicación de Matt está perfectamente bien, y toma una oportunidad en comparación con C y Java, lo cual no haré, pero por alguna razón realmente disfruto discutiendo este tema de vez en cuando, así que aquí está mi oportunidad en una respuesta

En los puntos (3) y (4):

Los puntos (3) y (4) en su lista parecen ser los más interesantes y relevantes ahora.

Para comprenderlos, es útil tener una idea clara de lo que sucede con el código Lisp, en forma de una secuencia de caracteres ingresados ​​por el programador, en camino a su ejecución. Usemos un ejemplo concreto:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

Este fragmento de código Clojure se imprime aFOObFOOcFOO. Tenga en cuenta que podría decirse que Clojure no satisface completamente el cuarto punto de su lista, ya que el tiempo de lectura no está realmente abierto al código del usuario; Sin embargo, discutiré lo que significaría que esto sea de otra manera.

Entonces, supongamos que tenemos este código en un archivo en algún lugar y le pedimos a Clojure que lo ejecute. Además, supongamos (por simplicidad) que hemos superado la importación de la biblioteca. La parte interesante comienza en (printlny termina en el )extremo derecho. Esto es lexed / parsed como uno esperaría, pero ya surge un punto importante: el resultado no es una representación AST específica del compilador específica: es solo una estructura de datos Clojure / Lisp normal , es decir, una lista anidada que contiene un montón de símbolos, cadenas y, en este caso, un único objeto de patrón regex compilado correspondiente a la#"\d+"literal (más sobre esto a continuación). Algunos Lisps agregan sus propios pequeños giros a este proceso, pero Paul Graham se refería principalmente a Common Lisp. En los puntos relevantes para su pregunta, Clojure es similar a CL.

Todo el lenguaje en tiempo de compilación:

Después de este punto, todo lo que trata el compilador (esto también sería cierto para un intérprete de Lisp; el código Clojure siempre se compila) son estructuras de datos de Lisp que los programadores de Lisp están acostumbrados a manipular. En este punto, se hace evidente una posibilidad maravillosa: ¿por qué no permitir que los programadores de Lisp escriban funciones de Lisp que manipulan datos de Lisp que representan programas de Lisp y generan datos transformados que representan programas transformados, para ser utilizados en lugar de los originales? En otras palabras, ¿por qué no permitir que los programadores de Lisp registren sus funciones como complementos de compilación, llamados macros en Lisp? Y, de hecho, cualquier sistema Lisp decente tiene esta capacidad.

Entonces, las macros son funciones regulares de Lisp que operan en la representación del programa en tiempo de compilación, antes de la fase final de compilación cuando se emite el código objeto real. Dado que no hay límites en los tipos de código que se permite ejecutar las macros (en particular, el código que ejecutan a menudo se escribe con el uso liberal de la función de macro), se puede decir que "todo el lenguaje está disponible en tiempo de compilación ".

Todo el idioma en el momento de la lectura:

Volvamos a ese #"\d+"literal regex. Como se mencionó anteriormente, esto se transforma en un objeto de patrón compilado real en el momento de la lectura, antes de que el compilador escuche la primera mención del nuevo código que se está preparando para la compilación. ¿Como sucedió esto?

Bueno, la forma en que Clojure se implementa actualmente, la imagen es algo diferente de lo que Paul Graham tenía en mente, aunque todo es posible con un truco inteligente . En Common Lisp, la historia sería un poco más limpia conceptualmente. Sin embargo, los conceptos básicos son similares: el Lisp Reader es una máquina de estados que, además de realizar transiciones de estado y eventualmente declarar si ha alcanzado un "estado de aceptación", escupe las estructuras de datos de Lisp que representan los caracteres. Por lo tanto, los caracteres se 123convierten en el número, 123etc. El punto importante viene ahora: esta máquina de estado puede modificarse por código de usuario. (Como se señaló anteriormente, eso es completamente cierto en el caso de CL; para Clojure, se requiere un truco (desalentado y no utilizado en la práctica). Pero estoy divagando, es el artículo de PG sobre el que se supone que estoy elaborando, así que ...)

Entonces, si eres un programador de Common Lisp y te gusta la idea de los literales vectoriales de estilo Clojure, puedes conectar al lector una función para reaccionar adecuadamente a alguna secuencia de caracteres, [o #[posiblemente, y tratarla como El comienzo de un vector literal que termina en la coincidencia ]. Dicha función se denomina macro de lector y, al igual que una macro normal, puede ejecutar cualquier tipo de código Lisp, incluido el código que ha sido escrito con notación funky habilitada por macros de lector registradas previamente. Así que hay todo el lenguaje en el momento de la lectura para ti.

Envolviendolo:

En realidad, lo que se ha demostrado hasta ahora es que uno puede ejecutar funciones regulares de Lisp en tiempo de lectura o tiempo de compilación; el único paso que se debe tomar desde aquí para comprender cómo la lectura y la compilación son posibles en el tiempo de lectura, compilación o ejecución es darse cuenta de que la lectura y la compilación son realizadas por las funciones de Lisp. Puede llamar reado evalen cualquier momento para leer datos de Lisp de secuencias de caracteres o compilar y ejecutar el código de Lisp, respectivamente. Ese es todo el lenguaje allí, todo el tiempo.

Observe cómo el hecho de que Lisp satisface el punto (3) de su lista es esencial para la forma en que logra satisfacer el punto (4): el sabor particular de las macros proporcionadas por Lisp depende en gran medida del código representado por datos regulares de Lisp, que es algo habilitado por (3). Por cierto, solo el aspecto de "código de árbol" del código es realmente crucial aquí: posiblemente podría tener un Lisp escrito usando XML.

Michał Marczyk
fuente
44
Cuidado: al decir "macro regular (compilador)", estás a punto de implicar que las macros del compilador son macros "normales", cuando en Common Lisp (al menos), "macro del compilador" es algo muy específico y diferente: lispworks. com / documentation / lw51 / CLHS / Body / ...
Ken
Ken: Buena captura, gracias! Cambiaré eso a "macro regular", lo que creo que es poco probable que haga tropezar a alguien.
Michał Marczyk
Fantástica respuesta. Aprendí más de ello en 5 minutos que en horas buscando en Google / reflexionando sobre la pregunta. Gracias.
Charlie Flowers
Editar: argh, entendió mal una oración de continuación. Corregido por gramática (se necesita un "par" para aceptar mi edición).
Tatiana Racheva
Las expresiones S y XML pueden dictar las mismas estructuras, pero XML es mucho más detallado y, por lo tanto, no es adecuado como sintaxis.
Sylwester
66

1) Un nuevo concepto de variables. En Lisp, todas las variables son efectivamente punteros. Los valores son los que tienen tipos, no variables, y asignar o vincular variables significa copiar punteros, no a lo que apuntan.

(defun print-twice (it)
  (print it)
  (print it))

'it' es una variable. Se puede vincular a CUALQUIER valor. No hay restricción ni tipo asociado con la variable. Si llama a la función, no es necesario copiar el argumento. La variable es similar a un puntero. Tiene una forma de acceder al valor que está vinculado a la variable. No hay necesidad de reservar memoria. Podemos pasar cualquier objeto de datos cuando llamamos a la función: cualquier tamaño y cualquier tipo.

Los objetos de datos tienen un 'tipo' y todos los objetos de datos pueden consultarse por su 'tipo'.

(type-of "abc")  -> STRING

2) Un tipo de símbolo. Los símbolos difieren de las cadenas en que puede probar la igualdad comparando un puntero.

Un símbolo es un objeto de datos con un nombre. Por lo general, el nombre se puede usar para encontrar el objeto:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Como los símbolos son objetos de datos reales, podemos probar si son el mismo objeto:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Esto nos permite, por ejemplo, escribir una oración con símbolos:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Ahora podemos contar el número de THE en la oración:

(count 'the *sentence*) ->  2

En Common Lisp, los símbolos no solo tienen un nombre, sino que también pueden tener un valor, una función, una lista de propiedades y un paquete. Entonces los símbolos se pueden usar para nombrar variables o funciones. La lista de propiedades se usa generalmente para agregar metadatos a los símbolos.

3) Una notación para el código usando árboles de símbolos.

Lisp usa sus estructuras de datos básicas para representar el código.

La lista (* 3 2) puede ser tanto datos como código:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

El árbol:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Todo el idioma siempre disponible. No existe una distinción real entre tiempo de lectura, tiempo de compilación y tiempo de ejecución. Puede compilar o ejecutar código mientras lee, leer o ejecutar código mientras compila, y leer o compilar código en tiempo de ejecución.

Lisp proporciona las funciones LEER para leer datos y código del texto, CARGAR para cargar el código, EVAL para evaluar el código, COMPILAR para compilar el código e IMPRIMIR para escribir datos y código para el texto.

Estas funciones están siempre disponibles. No se van Pueden ser parte de cualquier programa. Eso significa que cualquier programa puede leer, cargar, evaluar o imprimir código, siempre.

¿Cómo son diferentes en lenguajes como C o Java?

Esos idiomas no proporcionan símbolos, códigos como datos o evaluación de datos en tiempo de ejecución como código. Los objetos de datos en C generalmente no están tipificados.

¿Algún otro idioma que no sea el lenguaje familiar LISP tiene alguna de estas construcciones ahora?

Muchos idiomas tienen algunas de estas capacidades.

La diferencia:

En Lisp, estas capacidades están diseñadas en el lenguaje para que sean fáciles de usar.

rev. Rainer Joswig
fuente
33

Para los puntos (1) y (2), él está hablando históricamente. Las variables de Java son más o menos las mismas, por eso necesita llamar a .equals () para comparar valores.

(3) está hablando de expresiones S. Los programas Lisp están escritos en esta sintaxis, que ofrece muchas ventajas sobre la sintaxis ad-hoc como Java y C, como capturar patrones repetidos en macros de una manera mucho más limpia que las macros C o las plantillas C ++, y manipular código con la misma lista principal operaciones que usa para los datos.

(4) tomando C por ejemplo: el lenguaje es realmente dos idiomas diferentes: cosas como if () y while (), y el preprocesador. Utiliza el preprocesador para evitar tener que repetirse todo el tiempo u omitir código con # if / # ifdef. Pero ambos idiomas están bastante separados, y no puedes usar while () en tiempo de compilación como puedes #if.

C ++ lo hace aún peor con las plantillas. Consulte algunas referencias sobre la metaprogramación de plantillas, que proporciona una forma de generar código en el momento de la compilación, y es extremadamente difícil para los no expertos entenderlo. Además, es realmente un montón de trucos y trucos que usan plantillas y macros para los que el compilador no puede proporcionar soporte de primera clase: si comete un error de sintaxis simple, el compilador no puede darle un mensaje de error claro.

Bueno, con Lisp, tienes todo esto en un solo idioma. Utiliza lo mismo para generar código en tiempo de ejecución mientras aprende en su primer día. Esto no sugiere que la metaprogramación sea trivial, pero ciertamente es más sencilla con un lenguaje de primera clase y soporte de compilación.

Matt Curtis
fuente
77
Ah, también, este poder (y simplicidad) ahora tiene más de 50 años y es tan fácil de implementar que un programador novato puede aprovecharlo con una guía mínima y aprender sobre los fundamentos del lenguaje. ¡No escucharías un reclamo similar de Java, C, Python, Perl, Haskell, etc. como un buen proyecto para principiantes!
Matt Curtis
9
No creo que las variables de Java sean como los símbolos de Lisp en absoluto. No hay notación para un símbolo en Java, y lo único que puede hacer con una variable es obtener su celda de valor. Las cadenas pueden ser internadas, pero no suelen ser nombres, por lo que ni siquiera tiene sentido hablar sobre si se pueden citar, evaluar, aprobar, etc.
Ken
2
Más de 40 años podrían ser más precisos :), @Ken: Creo que quiere decir que 1) las variables no primitivas en java son por referencia, que es similar a lisp y 2) las cadenas internadas en java son similares a los símbolos en lisp - por supuesto, como dijiste, no puedes citar o evaluar cadenas / códigos internos en Java, por lo que siguen siendo bastante diferentes.
3
@Dan: no estoy seguro de cuándo se realizó la primera implementación, pero el artículo inicial de McCarthy sobre computación simbólica se publicó en 1960.
Inaimathi
Java tiene soporte parcial / irregular para "símbolos" en forma de Foo.class / foo.getClass (), es decir, un objeto de tipo <Foo> de tipo de tipo es un poco análogo, al igual que los valores de enumeración, para un grado Pero sombras muy mínimas de un símbolo Lisp.
BRPocock
-3

Los puntos (1) y (2) también encajarían en Python. Tomando un ejemplo simple "a = str (82.4)", el intérprete crea primero un objeto de coma flotante con el valor 82.4. Luego llama a un constructor de cadenas que luego devuelve una cadena con el valor '82 .4 '. La 'a' en el lado izquierdo es simplemente una etiqueta para ese objeto de cadena. El objeto de coma flotante original era basura recolectada porque no hay más referencias a él.

En Scheme, todo se trata como un objeto de manera similar. No estoy seguro acerca de Common Lisp. Trataría de evitar pensar en términos de conceptos C / C ++. Me desaceleraron cuando intentaba entender la hermosa simplicidad de Lisps.

CyberFonic
fuente