De acuerdo con este artículo, la siguiente línea de código Lisp imprime "Hola mundo" a la salida estándar.
(format t "hello, world")
Lisp, que es un lenguaje homoicónico , puede tratar el código como datos de esta manera:
Ahora imagine que escribimos la siguiente macro:
(defmacro backwards (expr) (reverse expr))
al revés es el nombre de la macro, que toma una expresión (representada como una lista) y la invierte. Aquí está "Hola, mundo" nuevamente, esta vez usando la macro:
(backwards ("hello, world" t format))
Cuando el compilador de Lisp ve esa línea de código, mira el primer átomo en la lista (
backwards
) y nota que nombra una macro. Pasa la lista no evaluada("hello, world" t format)
a la macro, que reorganiza la lista(format t "hello, world")
. La lista resultante reemplaza la expresión de macro, y es lo que se evaluará en tiempo de ejecución. El entorno Lisp verá que su primer átomo (format
) es una función, y lo evaluará, pasándole el resto de los argumentos.
En Lisp lograr esta tarea es fácil (corrígeme si me equivoco) porque el código se implementa como una lista (¿ s-expresiones ?)
Ahora eche un vistazo a este fragmento de OCaml (que no es un lenguaje homoicónico):
let print () =
let message = "Hello world" in
print_endline message
;;
Imagine que desea agregar homoiconicidad a OCaml, que utiliza una sintaxis mucho más compleja en comparación con Lisp. ¿Cómo lo harías tú? ¿El lenguaje tiene que tener una sintaxis particularmente fácil para lograr la homoiconicidad?
EDITAR : a partir de este tema encontré otra forma de lograr la homoiconicidad que es diferente de la de Lisp: la implementada en el lenguaje io . Puede responder parcialmente esta pregunta.
Aquí, comencemos con un bloque simple:
Io> plus := block(a, b, a + b) ==> method(a, b, a + b ) Io> plus call(2, 3) ==> 5
Bien, entonces el bloque funciona. El bloque más agregó dos números.
Ahora hagamos una introspección sobre este pequeño compañero.
Io> plus argumentNames ==> list("a", "b") Io> plus code ==> block(a, b, a +(b)) Io> plus message name ==> a Io> plus message next ==> +(b) Io> plus message next name ==> +
Molde caliente sagrado frío. No solo puede obtener los nombres de los parámetros de bloque. Y no solo puede obtener una cadena del código fuente completo del bloque. Puede colarse en el código y atravesar los mensajes dentro. Y lo más sorprendente de todo: es terriblemente fácil y natural. Fiel a la búsqueda de Io. El espejo de Ruby no puede ver nada de eso.
Pero, whoa whoa, hey ahora, no toques ese dial.
Io> plus message next setName("-") ==> -(b) Io> plus ==> method(a, b, a - b ) Io> plus call(2, 3) ==> -1
Respuestas:
Puedes hacer cualquier idioma homoicónico. Esencialmente, hace esto 'reflejando' el lenguaje (es decir, para cualquier constructor de lenguaje, agregue una representación correspondiente de ese constructor como datos, piense en AST). También debe agregar un par de operaciones adicionales, como las citas y las citas. Eso es más o menos eso.
Lisp tuvo eso al principio debido a su sintaxis fácil, pero la familia de idiomas MetaML de W. Taha demostró que es posible hacerlo para cualquier idioma.
Todo el proceso se describe en Modelado de metaprogramación generativa homogénea . Aquí hay una introducción más ligera al mismo material .
fuente
El compilador de Ocaml está escrito en el propio Ocaml, por lo que ciertamente hay una manera de manipular los AST de Ocaml en Ocaml.
Uno podría imaginar agregar un tipo incorporado
ocaml_syntax
al idioma y tener unadefmacro
función incorporada, que toma una entrada de tipo, digamosAhora, ¿cuál es el tipo de
defmacro
? Bueno, eso realmente depende de la entrada, ya que incluso sif
es la función de identidad, el tipo de código resultante depende del fragmento de sintaxis que se pasa.Este problema no surge en lisp, ya que el lenguaje se escribe dinámicamente y no es necesario asignar ningún tipo a la macro en tiempo de compilación. Una solución sería tener
lo que permitiría utilizar la macro en cualquier contexto. Pero esto no es seguro, por supuesto, permitiría
bool
usar a en lugar de astring
, bloqueando el programa en tiempo de ejecución.La única solución basada en principios en un lenguaje de tipo estático sería tener tipos dependientes en los que el tipo de resultado
defmacro
dependería de la entrada. Sin embargo, las cosas se ponen bastante complicadas en este punto, y comenzaría señalando la agradable disertación de David Raymond Christiansen.En conclusión: tener una sintaxis complicada no es un problema, ya que hay muchas formas de representar la sintaxis dentro del lenguaje y, posiblemente, utilizar la metaprogramación como una
quote
operación para integrar la sintaxis "simple" en el internoocaml_syntax
.El problema es hacer esto bien escrito, en particular tener un mecanismo de macro en tiempo de ejecución que no permita errores de tipo.
Por supuesto, es posible tener un mecanismo de tiempo de compilación para macros en un lenguaje como Ocaml, véase, por ejemplo, MetaOcaml .
También posiblemente útil: Jane street sobre metaprogramación en Ocaml
fuente
Como ejemplo, considere F # (basado en OCaml). F # no es completamente homoicónico, pero admite obtener el código de una función como AST bajo ciertas circunstancias.
En F #, su
print
se representaría como unExpr
que se imprime como:Para resaltar mejor la estructura, aquí hay una forma alternativa de cómo podría crear la misma
Expr
:fuente
eval(<string>)
función? ( Según muchos recursos, tener una función de evaluación es diferente de tener homoiconicidad , ¿es la razón por la que dijiste que F # no es completamente homoicónico?)print
con el[<ReflectedDefinition>]
atributo.)