haskell macros lisp scheme

haskell - ¿Hasta qué punto las macros “funcionan a la inversa”?



lisp scheme (6)

Las respuestas son "no" y "sí".

Parece que has comenzado con un buen modelo de macros, donde el nivel de macro y el nivel de tiempo de ejecución están en mundos separados. De hecho, este es uno de los puntos principales detrás del sistema de macros de Racket . Puede leer un breve texto al respecto en la guía de Racket , o ver el documento original que describe esta característica y por qué es una buena idea hacerlo. Tenga en cuenta que el sistema macro de Racket es muy sofisticado y es higiénico, pero la separación de fases es una buena idea, independientemente de la higiene. Para resumir la ventaja principal, es posible expandir siempre el código de manera confiable, para que obtenga beneficios como una compilación separada, y no dependa del orden de carga del código ni de tales problemas.

Entonces, te mudaste a un solo entorno, lo que pierde eso. En la mayor parte del mundo Lisp (por ejemplo, en CL y en Elisp), esto es exactamente cómo se hacen las cosas, y obviamente, te encuentras con los problemas que se describen arriba. ("Obvio", ya que la separación de fases se diseñó para evitar estos problemas, usted acaba de obtener sus descubrimientos en el orden opuesto a como ocurrieron históricamente). En cualquier caso, para abordar algunos de estos problemas, existe la forma especial , que puede especificar que algunos códigos se evalúen en tiempo de ejecución o en tiempo de expansión de macro. En Elisp, obtienes eso con eval-when-compile , pero en CL obtienes mucho más cabello, con algunos otros "* -time". (CL también tiene tiempo de lectura, y tener que compartir el mismo entorno que todo lo demás es triplicar la diversión). Aunque parezca una buena idea, debería leer y ver cómo algunos tartamudos pierden cabello debido a este desastre .

Y en el último paso de su descripción, retrocede aún más en el tiempo y descubre algo que se conoce como FEXPRs. Ni siquiera voy a poner ningún indicio para eso, puedes encontrar un montón de textos al respecto, por qué algunas personas piensan que es realmente una mala idea, por qué otras personas piensan que es realmente una buena idea. Hablando en términos prácticos, esos dos "algunos" son "la mayoría" y "pocos" respectivamente, aunque los pocos bastiones FEXPR restantes pueden ser vocales. Para traducir todo esto: es algo explosivo ... Hacer preguntas acerca de esto es una buena manera de conseguir largas visitas al fuego. (Como ejemplo reciente de una discusión seria, puede ver el período de discusión inicial para el R7RS, donde surgieron los FEXPR y conducir exactamente a este tipo de llamas). No importa de qué lado elija sentarse, una cosa es obvia: a El lenguaje con FEXPRs es extremadamente diferente de un lenguaje sin ellos. [Coincidentemente, trabajar en una implementación en Haskell puede afectar tu vista, ya que tienes un lugar al que ir para un mundo estático sano para el código, por lo que la tentación en lenguajes súper dinámicos "lindos" es probablemente más grande ...]

Una última nota: dado que estás haciendo algo similar, deberías considerar un proyecto similar para implementar un Esquema en Haskell - IIUC, incluso tiene macros higiénicas.

Estoy escribiendo un Lisp en Haskell ( código en GitHub ) como una forma de aprender más sobre ambos idiomas.

La característica más nueva que estoy agregando es macros. Macros no higiénicos ni nada sofisticado, solo simples transformaciones de código vainilla. Mi implementación inicial tenía un entorno de macros separado, distinto del entorno en el que viven todos los demás valores. Entre las funciones de read y eval , intercalaba otra función, macroExpand , que recorría el árbol de códigos y realizaba las transformaciones apropiadas cada vez que encontraba una palabra clave en el entorno macro, antes de pasar la forma final a eval para ser evaluado. Una buena ventaja de esto fue que las macros tenían la misma representación interna que otras funciones, lo que reducía la duplicación de código.

Sin embargo, tener dos entornos parecía torpe y me molestaba que si quería cargar un archivo, eval tenía que tener acceso al entorno de macros en caso de que el archivo contuviera definiciones de macros. Así que decidí introducir un tipo de macro, almacenar macros en el mismo entorno que las funciones y variables, e incorporar la fase de expansión de macro en eval . Al principio no sabía cómo hacerlo, hasta que me di cuenta de que podía escribir este código:

eval env (List (function : args)) = do func <- eval env function case func of (Macro {}) -> apply func args >>= eval env _ -> mapM (eval env) args >>= apply func

Funciona de la siguiente manera:

  1. Si se le pasa una lista que contiene una expresión inicial y un grupo de otras expresiones ...
  2. Evaluar la primera expresión.
  3. Si es una macro, entonces aplíquela a los argumentos y evalúe el resultado
  4. Si no es una macro, evalúe los argumentos y aplique la función al resultado

Es como si las macros fueran exactamente iguales a las funciones, excepto que se cambia el orden de eval / apply.

¿Es esta una descripción precisa de las macros? ¿Me estoy perdiendo algo importante al implementar macros de esta manera? Si las respuestas son "sí" y "no", ¿por qué nunca antes he visto explicadas las macros de esta manera?


Lo que falta es que esta simetría se rompe cuando separa el análisis de la evaluación , que es lo que hacen todas las implementaciones prácticas de Lisp. La expansión de macros se produciría durante la fase de análisis, por lo que la eval se puede mantener simple.


No exactamente. En realidad, ha descrito bastante concisamente la diferencia entre "llamada por nombre" y "llamada por valor"; un lenguaje de llamada por valor reduce los argumentos a los valores antes de la sustitución, un lenguaje de llamada por nombre realiza primero la sustitución y luego la reducción.

La diferencia clave es que las macros le permiten romper la transparencia referencial; en particular, la macro puede examinar el código, y por lo tanto puede diferenciar entre (3 + 4) y 7, de una manera que el código ordinario no puede. Es por eso que las macros son más poderosas y también más peligrosas; la mayoría de los programadores estarían molestos si encontraran que (f 7) produjo un resultado y (f (+ 3 4)) produjo un resultado diferente.


Para lo que vale la pena, la construcción de enlaces de la sección Scheme R 5 RS para palabras clave sintácticas tiene esto que decir al respecto:

Let-syntax y letrec-syntax son análogos a let y letrec , pero unen palabras clave sintácticas a transformadores de macro en lugar de variables de enlace a ubicaciones que contienen valores.

Consulte: http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html#%_sec_4.3.1

Esto parece implicar que se debe usar una estrategia separada, al menos para el sistema de macros de syntax-rules .

Puede escribir algunos ... códigos interesantes en un Esquema que usa "lugares" separados para las macros. No tiene mucho sentido mezclar macros y variables del mismo nombre en cualquier código "real", pero si solo quieres probarlo, considera este ejemplo de Chicken Scheme:

#;1> let Error: unbound variable: let #;1> (define let +) #;2> (let ((talk "hello!")) (write talk)) "hello!" #;3> let #<procedure C_plus> #;4> (let 1 2) Error: (let) not a proper list: (let 1 2) Call history: <syntax> (let 1 2) <-- #;4> (define a let) #;5> (a 1 2) 3



Fondo de deambulación

Lo que tienes ahí son macros de enlace muy tardías. Este es un enfoque viable, pero es ineficiente, porque las ejecuciones repetidas del mismo código expandirán las macros repetidamente.

En el lado positivo, esto es amigable para el desarrollo interactivo. Si el programador cambia una macro y luego invoca un código que lo usa, como una función previamente definida, la nueva macro entra en vigor al instante. Este es un comportamiento intuitivo de "haz lo que quiero decir".

Bajo un sistema de macros que expande las macros antes, el programador debe redefinir todas las funciones que dependen de una macro cuando esa macro cambia, de lo contrario las definiciones existentes continúan basándose en las expansiones de macros antiguas, ajenas a la nueva versión de la macro .

Un enfoque razonable es tener este sistema macro de enlace tardío para el código interpretado, pero un sistema macro "regular" (por falta de una palabra mejor) para el código compilado.

La expansión de macros no requiere un entorno separado. No debería, porque las macros locales deberían estar en el mismo espacio de nombres que las variables. Por ejemplo, en Common Lisp, si hacemos esto (let (x) (symbol-macrolet ((x ''foo)) ...)) , la macro de símbolo interno sombrea la variable léxica exterior. El expansor de macro tiene que ser consciente de las formas de enlace de variables. ¡Y viceversa! Si hay un symbol-macrolet interno para la variable x , sombrea un symbol-macrolet . El expansor de macros no puede sustituir ciegamente todas las apariciones de x que ocurren en el cuerpo. En otras palabras, la expansión de macros de Lisp debe ser consciente del entorno léxico completo en el que coexisten las macros y otros tipos de enlaces. Por supuesto, durante la expansión de macros, no se crea una instancia del entorno de la misma manera. Por supuesto, si hay un (let ((x (function)) ..) , (function) no se llama y x no se le asigna un valor. Pero el expansor de macro es consciente de que hay una x en este entorno y, por lo tanto, ocurren de x no son macros.

Entonces, cuando decimos un entorno, lo que realmente queremos decir es que hay dos manifestaciones o instancias diferentes de un entorno unificado: la manifestación del tiempo de expansión y luego la manifestación del tiempo de evaluación. Las macros de enlace tardío simplifican la implementación al fusionar estas dos veces en una, como lo ha hecho, pero no tiene que ser así.

También tenga en cuenta que las macros de Lisp pueden aceptar un parámetro &environment . Esto es necesario si las macros necesitan llamar a macroexpand en algún fragmento de código suministrado por el usuario. Dicha recursión en el expansor de macros a través de una macro debe pasar el entorno adecuado para que el código del usuario tenga acceso a las macros que lo rodean de forma léxica y se expanda correctamente.

Ejemplo concreto

Supongamos que tenemos este código:

(symbol-macrolet ((x (+ 2 2))) (print x) (let ((x 42) (y 19)) (print x) (symbol-macrolet ((y (+ 3 3))) (print y))))

El efecto de esto para las impresiones 4 , 42 y 6 . Usemos la implementación CLISP de Common Lisp y expandamos esto utilizando la función específica de la implementación de CLISP llamada system::expand-form . No podemos usar una macroexpand estándar y macroexpand porque eso no retrocederá en las macros locales:

(system::expand-form ''(symbol-macrolet ((x (+ 2 2))) (print x) (let ((x 42) (y 19)) (print x) (symbol-macrolet ((y (+ 3 3))) (print y))))) --> (LOCALLY ;; this code was reformatted by hand to fit your screen (PRINT (+ 2 2)) (LET ((X 42) (Y 19)) (PRINT X) (LOCALLY (PRINT (+ 3 3))))) ;

(Ahora primero, sobre estas formas locally . ¿Por qué están allí? Tenga en cuenta que corresponden a lugares donde tuvimos un symbol-macrolet . Esto es probablemente por el bien de las declaraciones. Si el cuerpo de una forma de symbol-macrolet tiene declaraciones, tiene que estar dentro del alcance de ese cuerpo, y eso lo hará locally . Si la expansión de symbol-macrolet no deja este envoltorio locally , las declaraciones tendrán un alcance incorrecto.)

Desde esta expansión macro puede ver cuál es la tarea. El expansor de macros tiene que recorrer el código y reconocer todas las construcciones de enlace (realmente todas las formas especiales), no solo las construcciones de enlace que tienen que ver con el sistema de macros.

Observe cómo se deja solo una de las instancias de (print x) : la que se encuentra en el ámbito de (let ((x ..)) ...) . El otro se convirtió en (print (+ 2 2)) , de acuerdo con la macro de símbolo para x .

Otra cosa que podemos aprender de esto es que la expansión de macros simplemente sustituye la expansión y elimina las formas de symbol-macrolet . Por lo tanto, el entorno que queda es el original, menos todo el material macro que se elimina en el proceso de expansión. La expansión macro honra todos los enlaces léxicos, en un gran entorno "Grand Unified", pero luego se vaporiza gentilmente, dejando solo el código como (print (+ 2 2)) y otros vestigios como el (locally ...) , con solo las construcciones de enlace que no son de macro, se obtiene una versión reducida del entorno original.

Así, ahora, cuando se evalúa el código expandido, solo entra en juego la personalidad del entorno reducido. Los enlaces de let crean instancias y se rellenan con valores iniciales, etc. Durante la expansión, nada de eso estaba sucediendo; los enlaces que no son de macro solo se encuentran allí, afirmando su alcance, e insinuando una existencia futura en el tiempo de ejecución.