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 un try
bloque y luego llamaría EndUpdate()
:
macro UIUpdate(value as Expression):
return [|
$value.BeginUpdate()
try:
$(UIUpdate.Body)
ensure:
$value.EndUpdate()
|]
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 el MacroStatement
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:
UIUpdate myComboBox:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
y que se expanda a:
myComboBox.BeginUpdate()
try:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
ensure:
myComboBox.EndUpdate()
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 el Arguments
y Body
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 de MacroStatement
, y si intentas codificar algo que no es válido para un MacroStatement
, 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ón
Injertar 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!