Estoy aprendiendo Scheme del SICP y tengo la impresión de que gran parte de lo que hace a Scheme y, aún más, LISP especial es el sistema macro. Pero, dado que las macros se expanden en tiempo de compilación, ¿por qué la gente no crea sistemas de macros equivalentes para C / Python / Java / lo que sea? Por ejemplo, uno podría vincular el python
comando expand-macros | python
o lo que sea. El código aún sería portátil para las personas que no usan el sistema macro, uno simplemente expandiría las macros antes de publicar el código. Pero no sé nada de eso, excepto las plantillas en C ++ / Haskell, que creo que no son lo mismo. ¿Qué pasa con LISP, en todo caso, hace que sea más fácil implementar sistemas macro?
21
Respuestas:
Muchos Lispers le dirán que lo que hace a Lisp especial es la homoiconicidad , lo que significa que la sintaxis del código se representa utilizando las mismas estructuras de datos que otros datos. Por ejemplo, aquí hay una función simple (usando la sintaxis de Scheme) para calcular la hipotenusa de un triángulo rectángulo con las longitudes laterales dadas:
Ahora, la homoiconicidad dice que el código anterior es realmente representable como una estructura de datos (específicamente, listas de listas) en el código Lisp. Por lo tanto, considere las siguientes listas y vea cómo se "pegan":
(define #2# #3#)
(hypot x y)
(sqrt #4#)
(+ #5# #6#)
(square x)
(square y)
Las macros le permiten tratar el código fuente como eso: listas de cosas. Cada uno de esos 6 "sublistas" contener punteros a otras listas, o a los símbolos (en este ejemplo:
define
,hypot
,x
,y
,sqrt
,+
,square
).Entonces, ¿cómo podemos usar la homoiconicidad para "separar" la sintaxis y crear macros? Aquí hay un ejemplo simple. Vuelva a implementar la
let
macro, que llamaremosmy-let
. Como recordatorio,debería expandirse a
Aquí hay una implementación que usa macros de "cambio de nombre explícito" del esquema † :
El
form
parámetro está vinculado a la forma real, por lo que para nuestro ejemplo, sería(my-let ((foo 1) (bar 2)) (+ foo bar))
. Entonces, analicemos el ejemplo:cadr
agarra la((foo 1) (bar 2))
parte del formulario.Luego, recuperamos el cuerpo de la forma.
cddr
agarra la((+ foo bar))
parte del formulario. (Tenga en cuenta que esto pretende capturar todas las subformas después del enlace; por lo tanto, si el formulario fueraentonces el cuerpo sería
((debug foo) (debug bar) (+ foo bar))
.)lambda
expresión y llamamos usando los enlaces y el cuerpo que hemos recopilado. El backtick se llama "cuasiquote", lo que significa tratar todo dentro de la cuasiquote como datos literales, excepto los bits después de las comas ("entre comillas").(rename 'lambda)
medios para usar ellambda
enlace en vigor cuando se define esta macro , en lugar de cualquierlambda
enlace que pueda existir cuando se usa esta macro . (Esto se conoce como higiene ).(map car bindings)
devoluciones(foo bar)
: el primer dato en cada uno de los enlaces.(map cadr bindings)
devoluciones(1 2)
: el segundo dato en cada uno de los enlaces.,@
hace "empalme", que se usa para expresiones que devuelven una lista: hace que los elementos de la lista se peguen en el resultado, en lugar de la lista misma.(($lambda (foo bar) (+ foo bar)) 1 2)
, donde$lambda
aquí se refiere al cambio de nombrelambda
.Directo, ¿verdad? ;-) (Si no es sencillo para usted, imagine lo difícil que sería implementar un sistema macro para otros idiomas).
Por lo tanto, puede tener sistemas macro para otros idiomas, si tiene una manera de poder "separar" el código fuente de una manera no complicada. Hay algunos intentos de esto. Por ejemplo, sweet.js hace esto para JavaScript.
† Para los Schemers experimentados que leen esto, elegí intencionalmente usar macros de cambio de nombre explícito como un compromiso medio entre los
defmacro
s utilizados por otros dialectos de Lisp ysyntax-rules
(que sería la forma estándar de implementar tal macro en Scheme). No quiero escribir en otros dialectos de Lisp, pero no quiero alienar a los no Schemers que no están acostumbradossyntax-rules
.Como referencia, aquí está la
my-let
macro que usasyntax-rules
:La
syntax-case
versión correspondiente se ve muy similar:La diferencia entre los dos es que todo lo que en
syntax-rules
tiene una implícita#'
aplicada, por lo que puede solamente tener pares de patrón / plantilla ensyntax-rules
, por lo tanto, es totalmente declarativa. En contraste, ensyntax-case
, el bit después del patrón es un código real que, al final, tiene que devolver un objeto de sintaxis (#'(...)
), pero también puede contener otro código.fuente
syntax-rules
, que es puramente declarativo. Para macros complicadas, puedo usarsyntax-case
, que es en parte declarativo y en parte procesal. Y luego está el cambio de nombre explícito, que es puramente de procedimiento. (La mayoría de las implementaciones de Scheme proporcionarán unosyntax-case
o ER. No he visto uno que proporcione ambos. Son equivalentes en potencia.)Una opinión disidente: la homoiconicidad de Lisp es mucho menos útil de lo que la mayoría de los fanáticos de Lisp te hacen creer.
Para comprender las macros sintácticas, es importante comprender los compiladores. El trabajo de un compilador es convertir el código legible por humanos en código ejecutable. Desde una perspectiva de muy alto nivel, esto tiene dos fases generales: análisis y generación de código .
El análisis es el proceso de leer el código, interpretarlo de acuerdo con un conjunto de reglas formales y transformarlo en una estructura de árbol, generalmente conocida como AST (Árbol de sintaxis abstracta). Para toda la diversidad entre los lenguajes de programación, esta es una característica común notable: esencialmente cada lenguaje de programación de propósito general se analiza en una estructura de árbol.
Codigo de GENERACION toma el AST del analizador como su entrada y lo transforma en código ejecutable mediante la aplicación de reglas formales. Desde una perspectiva de rendimiento, esta es una tarea mucho más simple; muchos compiladores de idiomas de alto nivel dedican el 75% o más de su tiempo al análisis.
Lo que hay que recordar sobre Lisp es que es muy, muy viejo. Entre los lenguajes de programación, solo FORTRAN es más antiguo que Lisp. En el pasado, el análisis (la parte lenta de la compilación) se consideraba un arte oscuro y misterioso. Los documentos originales de John McCarthy sobre la teoría de Lisp (cuando era solo una idea de que nunca pensó que podría implementarse como un lenguaje de programación real) describe una sintaxis algo más compleja y expresiva que las modernas "expresiones S en todas partes para todo". "notación. Eso ocurrió más tarde, cuando la gente intentaba implementarlo. Debido a que el análisis no se entendía bien en ese entonces, básicamente lo golpearon y redujeron la sintaxis a una estructura de árbol homoicónica para hacer que el trabajo del analizador sea completamente trivial. El resultado final es que usted (el desarrollador) tiene que hacer mucho del analizador ' s trabaje para ello escribiendo el AST formal directamente en su código. ¡La homoiconicidad no "hace que las macros sean mucho más fáciles", sino que hace que escribir todo lo demás sea mucho más difícil!
El problema con esto es que, especialmente con la escritura dinámica, es muy difícil para las expresiones S transportar mucha información semántica. Cuando toda su sintaxis es del mismo tipo de cosas (listas de listas), no hay mucho en el contexto proporcionado por la sintaxis, por lo que el sistema macro tiene muy poco con qué trabajar.
La teoría del compilador ha recorrido un largo camino desde la década de 1960, cuando se inventó Lisp, y aunque las cosas que logró fueron impresionantes para su época, ahora parecen bastante primitivas. Para ver un ejemplo de un sistema moderno de metaprogramación, eche un vistazo al lenguaje Boo (tristemente subestimado). Boo es de tipo estático, orientado a objetos y de código abierto, por lo que cada nodo AST tiene un tipo con una estructura bien definida en la que un desarrollador de macros puede leer el código. El lenguaje tiene una sintaxis relativamente simple inspirada en Python, con varias palabras clave que dan un significado semántico intrínseco a las estructuras de árbol construidas a partir de ellas, y su metaprogramación tiene una sintaxis de cuasiquote intuitiva para simplificar la creación de nuevos nodos AST.
Aquí hay una macro que creé ayer cuando me di cuenta de que estaba aplicando el mismo patrón a un montón de lugares diferentes en el código GUI, donde llamaría
BeginUpdate()
a un control UI, realizaría una actualización en untry
bloque y luego llamaríaEndUpdate()
:El
macro
comando es, de hecho, una macro en sí , una que toma un cuerpo de macro como entrada y genera una clase para procesar la macro. Utiliza el nombre de la macro como una variable que representa elMacroStatement
nodo AST que representa la invocación de la macro. El [| ... |] es un bloque de cuasiquote, que genera el AST que corresponde al código dentro y dentro del bloque de cuasiquote, el símbolo $ proporciona la función "sin comillas", sustituyendo en un nodo como se especifica.Con esto, es posible escribir:
y que se expanda a:
Expresando la macro de esta manera es más sencillo y más intuitivo de lo que sería en una macro Lisp, porque el desarrollador conoce la estructura de
MacroStatement
y sabe cómo elArguments
yBody
propiedades de trabajo, y que el conocimiento inherente puede ser utilizado para expresar los conceptos involucrados en una muy intuitiva camino. También es más seguro, porque el compilador conoce la estructura deMacroStatement
, y si intentas codificar algo que no es válido para unMacroStatement
, el compilador lo detectará de inmediato e informará el error en lugar de que no lo sepas hasta que algo explote en ti. tiempo de ejecuciónInjertar macros en Haskell, Python, Java, Scala, etc. no es difícil porque estos lenguajes no son homicónicos; es difícil porque los idiomas no están diseñados para ellos, y funciona mejor cuando la jerarquía AST de su idioma está diseñada desde cero para ser examinada y manipulada por un sistema macro. Cuando trabajas con un lenguaje que fue diseñado teniendo en cuenta la metaprogramación desde el principio, ¡las macros son mucho más simples y fáciles de usar!
fuente
if...
por ejemplo, no parece una llamada de función. No conozco a Boo, pero imagina que Boo no tenía coincidencia de patrones, ¿podrías presentarlo con su propia sintaxis como macro? Mi punto es: cualquier nueva macro en Lisp se siente 100% natural, en otros idiomas funcionan, pero puedes ver las puntadas.¿Cómo es eso? Todo el código en SICP está escrito en estilo libre de macro. No hay macros en SICP. Solo en una nota al pie de la página 373 se mencionan las macros.
No son necesariamente Lisp proporciona macros tanto en intérpretes como en compiladores. Por lo tanto, puede que no haya un tiempo de compilación. Si tiene un intérprete Lisp, las macros se expanden en el momento de la ejecución. Dado que muchos sistemas Lisp tienen un compilador incorporado, uno puede generar código y compilarlo en tiempo de ejecución.
Probemos eso usando SBCL, una implementación de Common Lisp.
Cambiemos SBCL al intérprete:
Ahora definimos una macro. La macro imprime algo cuando se solicita un código expandido. El código generado no se imprime.
Ahora usemos la macro:
Ver. En el caso anterior, Lisp no hace nada. La macro no se expande en el momento de la definición.
Pero en tiempo de ejecución, cuando se usa el código, la macro se expande.
Nuevamente, en tiempo de ejecución, cuando se usa el código, la macro se expande.
Tenga en cuenta que SBCL se expandiría solo una vez al usar un compilador. Pero varias implementaciones de Lisp también proporcionan intérpretes, como SBCL.
¿Por qué las macros son fáciles en Lisp? Bueno, no son realmente fáciles. Solo en Lisps, y hay muchos, que tienen soporte de macro incorporado. Dado que muchos Lisps vienen con una amplia maquinaria para macros, parece que es fácil. Pero los mecanismos macro pueden ser extremadamente complicados.
fuente
eval
oload
codifica en cualquier lenguaje Lisp, las macros también serán procesadas. Mientras que si usa un sistema de preprocesador como se propone en su pregunta,eval
y similares no se beneficiarán de la expansión de macro.read
en Lisp. Esta distinción es importante, porqueeval
funciona en las estructuras de datos de la lista real (como se menciona en mi respuesta), no en la forma textual. Entonces puede usar(eval '(+ 1 1))
y recuperar 2, pero si lo hace(eval "(+ 1 1)")
, recupera"(+ 1 1)"
(la cadena). Se usaread
para ir de"(+ 1 1)"
(una cadena de 7 caracteres) a(+ 1 1)
(una lista con un símbolo y dos fixnums).read
tiempo. Funcionan en tiempo de compilación en el sentido de que si tiene un código similar(and (test1) (test2))
, se expandirá(if (test1) (test2) #f)
(en Scheme) solo una vez, cuando se carga el código, en lugar de cada vez que se ejecuta el código, pero si hace algo como(eval '(and (test1) (test2)))
, que compilará (y ampliará macro) esa expresión de manera apropiada, en tiempo de ejecución.eval
funcionan en cadenas de texto, y sus capacidades para la modificación de la sintaxis son mucho más deslucidas y / o engorrosas.La homoiconicidad hace que sea mucho más fácil implementar macros. La idea de que el código es datos y los datos son códigos hace posible que más o menos (salvo la captura accidental de identificadores, resueltos por macros higiénicos ) sustituyan libremente uno por el otro. Lisp y Scheme lo hacen más fácil con su sintaxis de expresiones S que están estructuradas de manera uniforme y, por lo tanto, fáciles de convertir en AST que forman la base de las macros sintácticas .
Los lenguajes sin S-Expressions o Homoiconicity se encontrarán con problemas para implementar Syntactic Macros, aunque todavía se puede hacer. El Proyecto Kepler está intentando presentarles Scala, por ejemplo.
El mayor problema con el uso de macros de sintaxis, aparte de la no homoiconicidad, es el problema de la sintaxis generada arbitrariamente. Ofrecen una gran flexibilidad y potencia, pero al precio de que su código fuente ya no sea tan fácil de entender o mantener.
fuente