Como hacer un idioma homoicónico

16

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
incud
fuente
1
Es posible que desee echar un vistazo a cómo Scala hizo sus macros
Bergi
1
@Bergi Scala tiene un nuevo enfoque para las macros: scala.meta .
Martin Berger
Siempre he pensado que la homoiconicidad está sobrevalorada. En cualquier lenguaje lo suficientemente potente, siempre puede definir una estructura de árbol que refleje la estructura del lenguaje en sí, y las funciones de utilidad se pueden escribir para traducir hacia y desde el idioma de origen (y / o una forma compilada) según sea necesario. Sí, es un poco más fácil en LISP, pero dado que (a) la gran mayoría del trabajo de programación no debería ser metaprogramación y (b) LISP ha sacrificado la claridad del lenguaje para hacer esto posible, no creo que la compensación valga la pena.
Periata Breatta
@PeriataBreatta Tienes razón, pero la ventaja clave de MP es que MP permite abstracciones sin penalización de tiempo de ejecución . De este modo, MP resuelve la tensión entre la abstracción y el rendimiento, aunque a costa de aumentar la complejidad del lenguaje. ¿Vale la pena? Diría que el hecho de que todos los PL principales tengan extensiones MP indica que muchos programadores que trabajan encuentran que las compensaciones que ofrece MP son útiles.
Martin Berger

Respuestas:

10

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 .

Martin Berger
fuente
1
Corrígeme si me equivoco. "reflejo" está relacionado con la segunda parte de la pregunta (homoiconicity in io lang), ¿verdad?
incud
@Ignus No estoy seguro de entender completamente tu comentario. El propósito de la homoiconicidad es permitir el tratamiento del código como datos. Eso significa que cualquier forma de código debe tener una representación como datos. Hay varias formas de hacerlo (por ejemplo, cuasi comillas AST, utilizando tipos para distinguir el código de los datos como lo hace el enfoque de puesta en escena modular liviano), pero todos requieren duplicar / reflejar la sintaxis del lenguaje de alguna forma.
Martin Berger
¿Asumo que @Ignus se beneficiaría de mirar MetaOCaml entonces? ¿Ser "homoicónico" significa ser cotizable entonces? ¿Asumo que los lenguajes de etapas múltiples como MetaML y MetaOCaml van más allá?
Steven Shaw
1
@StevenShaw MetaOCaml es muy interesante, especialmente el nuevo BOC MetaOCaml de Oleg . Sin embargo, está algo restringido porque solo realiza metaprogramación en tiempo de ejecución y representa código solo a través de cuasi-citas que no son tan expresivas como las AST.
Martin Berger
7

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_syntaxal idioma y tener una defmacrofunción incorporada, que toma una entrada de tipo, digamos

f : ocaml_syntax -> ocaml_syntax

Ahora, ¿cuál es el tipo de defmacro? Bueno, eso realmente depende de la entrada, ya que incluso si fes 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

defmacro : (ocaml_syntax -> ocaml_syntax) -> 'a

lo que permitiría utilizar la macro en cualquier contexto. Pero esto no es seguro, por supuesto, permitiría boolusar a en lugar de a string, 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 defmacrodependerí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 quoteoperación para integrar la sintaxis "simple" en el interno ocaml_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

cody
fuente
2
MetaOCaml tiene meta-programación en tiempo de ejecución, no meta-programación en tiempo de compilación. Además, el sistema de mecanografía de MetaOCaml no tiene tipos dependientes. (¡También se descubrió que MetaOCaml no tenía ningún tipo de letra!) Plantilla Haskell tiene un enfoque intermedio interesante: cada etapa es segura de tipo, pero al ingresar a una nueva etapa, debemos volver a hacer la verificación de tipo. En mi experiencia, esto funciona muy bien en la práctica, y no pierde los beneficios de la seguridad de tipos en la etapa final (tiempo de ejecución).
Martin Berger
@cody es posible tener metaprogramación en OCaml también con puntos de extensión , ¿verdad?
incud
@Ignus Me temo que no sé mucho sobre los puntos de extensión, aunque sí lo menciono en el enlace al blog de Jane Street.
cody
1
Mi compilador C está escrito en C, pero eso no significa que pueda manipular el AST en C ...
BlueRaja - Danny Pflughoeft
2
@immibis: Obviamente, pero si eso es lo que quiso decir, esa declaración es a la vez vacía y sin relación con la pregunta ...
BlueRaja - Danny Pflughoeft
1

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 printse representaría como un Exprque se imprime como:

Let (message, Value ("Hello world"), Call (None, print_endline, [message]))

Para resaltar mejor la estructura, aquí hay una forma alternativa de cómo podría crear la misma Expr:

let messageVar = Var("message", typeof<string>)
let expr = Expr.Let(messageVar,
                    Expr.Value("Hello world"),
                    Expr.Call(print_endline_method, [Expr.Var(messageVar)]))
svick
fuente
No lo entendí. ¿Quiere decir que F # le permite "construir" el AST de una expresión y luego ejecutarla? Si es así, ¿cuál es la diferencia con los idiomas que le permiten usar la 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?)
incud
@Ignus Puede construir el AST usted mismo, o puede dejar que el compilador lo haga. La homoiconicidad "permite acceder a todos los códigos del lenguaje y transformarlos como datos" . En F #, puede acceder a algunos códigos como datos. (Por ejemplo, debe marcar printcon el [<ReflectedDefinition>]atributo.)
svick