Comment rendre une langue homoiconique

16

Selon cet article, la ligne suivante de code Lisp imprime "Hello world" sur la sortie standard.

(format t "hello, world")

Le lisp, qui est un langage homoiconique , peut traiter le code comme des données de cette manière:

Imaginez maintenant que nous avons écrit la macro suivante:

(defmacro backwards (expr) (reverse expr))

en arrière est le nom de la macro, qui prend une expression (représentée sous forme de liste) et l'inverse. Voici à nouveau "Bonjour, monde", cette fois en utilisant la macro:

(backwards ("hello, world" t format))

Lorsque le compilateur Lisp voit cette ligne de code, il examine le premier atome de la liste ( backwards) et remarque qu'il nomme une macro. Il transmet la liste non évaluée ("hello, world" t format)à la macro, qui réorganise la liste en (format t "hello, world"). La liste résultante remplace l'expression de macro et c'est ce qui sera évalué au moment de l'exécution. L'environnement Lisp verra que son premier atome ( format) est une fonction et l'évaluera en lui passant le reste des arguments.

En Lisp, cette tâche est facile (corrigez-moi si je me trompe) car le code est implémenté sous forme de liste ( expressions s ?).

Jetez maintenant un œil à cet extrait de code OCaml (qui n'est pas un langage homoiconique):

let print () =
    let message = "Hello world" in
    print_endline message
;;

Imaginez que vous souhaitiez ajouter une homoiconicité à OCaml, qui utilise une syntaxe beaucoup plus complexe que Lisp. Comment feriez-vous cela? Le langage doit-il avoir une syntaxe particulièrement simple pour atteindre l'homoiconicité?

EDIT : à partir de ce sujet, j'ai trouvé un autre moyen d'atteindre l'homoiconicité qui est différent de Lisp: celui implémenté dans le langage io . Il peut répondre partiellement à cette question.

Ici, commençons par un simple bloc:

Io> plus := block(a, b, a + b)
==> method(a, b, 
        a + b
    )
Io> plus call(2, 3)
==> 5

D'accord, donc le bloc fonctionne. Le bloc plus a ajouté deux nombres.

Faisons maintenant une introspection sur ce petit bonhomme.

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
==> +

Moule chaud et froid. Non seulement vous pouvez obtenir les noms des paramètres de bloc. Et non seulement vous pouvez obtenir une chaîne du code source complet du bloc. Vous pouvez vous faufiler dans le code et parcourir les messages à l'intérieur. Et le plus étonnant de tous: c'est terriblement facile et naturel. Fidèle à la quête d'Io. Le miroir de Ruby ne peut rien voir de tout ça.

Mais, whoa whoa, hé maintenant, ne touchez pas ce cadran.

Io> plus message next setName("-")
==> -(b)
Io> plus
==> method(a, b, 
        a - b
    )
Io> plus call(2, 3)
==> -1
incud
la source
1
Vous voudrez peut-être voir comment Scala a fait ses macros
Bergi
1
@Bergi Scala a une nouvelle approche des macros: scala.meta .
Martin Berger
J'ai toujours pensé que l'homoiconicité était surfaite. Dans tout langage suffisamment puissant, vous pouvez toujours définir une structure arborescente qui reflète la structure du langage lui-même, et des fonctions utilitaires peuvent être écrites pour traduire vers et depuis le langage source (et / ou une forme compilée) selon les besoins. Oui, c'est un peu plus facile dans les LISP, mais étant donné que (a) la grande majorité du travail de programmation ne devrait pas être une métaprogrammation et (b) LISP a sacrifié la clarté du langage pour rendre cela possible, je ne pense pas que le compromis en vaille la peine.
Periata Breatta du
@PeriataBreatta Vous avez raison, mais l'avantage clé de MP est que MP permet des abstractions sans pénalité d'exécution . MP résout ainsi la tension entre abstraction et performance, mais au prix d'une complexité linguistique croissante. Est-ce que ça vaut le coup? Je dirais que le fait que tous les PL principaux aient des extensions MP indique que beaucoup de programmeurs qui travaillent trouvent les compromis que MP offre utiles.
Martin Berger

Réponses:

10

Vous pouvez rendre n'importe quelle langue homoiconique. Essentiellement, vous faites cela en «reflétant» le langage (ce qui signifie que pour tout constructeur de langage, vous ajoutez une représentation correspondante de ce constructeur en tant que données, pensez AST). Vous devez également ajouter quelques opérations supplémentaires telles que la citation et la non-cotation. C'est plus ou moins ça.

Lisp l'a fait très tôt en raison de sa syntaxe facile, mais la famille de langues MetaML de W. Taha a montré qu'il était possible de le faire pour n'importe quelle langue.

L'ensemble du processus est décrit dans Modélisation de la méta-programmation générative homogène . Une introduction plus légère au même matériau est ici .

Martin Berger
la source
1
Corrige moi si je me trompe. "miroir" est lié à la deuxième partie de la question (homoiconicité dans io lang), non?
incud
@Ignus, je ne suis pas sûr de bien comprendre votre remarque. L'homoiconicité a pour but de permettre le traitement du code en tant que données. Cela signifie que toute forme de code doit avoir une représentation sous forme de données. Il existe plusieurs façons de le faire (par exemple, les quasi-guillemets AST, en utilisant des types pour distinguer le code des données comme le fait l'approche de mise en scène modulaire légère), mais tous nécessitent un doublement / mise en miroir de la syntaxe du langage sous une forme ou une autre.
Martin Berger
Je suppose que @Ignus gagnerait à regarder MetaOCaml alors? Est-ce qu'être "homoiconique" signifie simplement être cité alors? Je suppose que les langages multi-étapes comme MetaML et MetaOCaml vont plus loin?
Steven Shaw
1
@StevenShaw MetaOCaml est très intéressant, en particulier le nouveau BER MetaOCaml d'Oleg . Cependant, il est quelque peu limité en ce qu'il n'effectue que de la méta-programmation au moment de l'exécution et ne représente le code que par des guillemets qui ne sont pas aussi expressifs que les AST.
Martin Berger
7

Le compilateur Ocaml est écrit en Ocaml lui-même, il y a donc certainement un moyen de manipuler les AST Ocaml dans Ocaml.

On pourrait imaginer ajouter un type intégré ocaml_syntaxau langage et avoir une defmacrofonction intégrée, qui prend une entrée de type, disons

f : ocaml_syntax -> ocaml_syntax

Maintenant, quel est le type de defmacro? Eh bien, cela dépend vraiment de l'entrée, car même si fc'est la fonction d'identité, le type du morceau de code résultant dépend du morceau de syntaxe transmis.

Ce problème ne se pose pas dans lisp, car le langage est typé dynamiquement et aucun type n'a besoin d'être attribué à la macro elle-même au moment de la compilation. Une solution serait d’avoir

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

ce qui permettrait à la macro d'être utilisée dans n'importe quel contexte. Mais ce n'est pas sûr, bien sûr, cela permettrait boolà a d'être utilisé à la place de a string, faisant planter le programme au moment de l'exécution.

La seule solution de principe dans un langage de type statique serait d'avoir des types dépendants dans lesquels le type de résultat defmacrodépendrait de l'entrée. Les choses deviennent assez compliquées à ce stade cependant, et je commencerais par vous signaler la belle dissertation de David Raymond Christiansen.

En conclusion: avoir une syntaxe compliquée n'est pas un problème, car il existe de nombreuses façons de représenter la syntaxe à l'intérieur du langage, et éventuellement d'utiliser la méta-programmation comme une quoteopération pour incorporer la syntaxe "simple" dans l'interne ocaml_syntax.

Le problème est de bien faire ce type, en particulier d'avoir un mécanisme de macro d'exécution qui ne permet pas les erreurs de type.

Il est bien sûr possible d' avoir un mécanisme de compilation pour les macros dans un langage comme Ocaml, voir par exemple MetaOcaml .

Peut-être aussi utile: Jane street sur la méta-programmation dans Ocaml

cody
la source
2
MetaOCaml a une méta-programmation à l'exécution, pas une méta-programmation à la compilation. Le système de frappe de MetaOCaml n'a pas non plus de types dépendants. (MetaOCaml s'est également avéré être de type non fiable!) Le modèle Haskell a une approche intermédiaire intéressante: chaque étape est sécurisée, mais lorsque vous entrez dans une nouvelle étape, nous devons refaire la vérification de type. D'après mon expérience, cela fonctionne très bien dans la pratique et vous ne perdez pas les avantages de la sécurité de type au stade final (au moment de l'exécution).
Martin Berger
@cody, il est possible d'avoir une métaprogrammation dans OCaml également avec des points d'extension , non?
incud
@Ignus Je crains de ne pas en savoir grand-chose sur les points d'extension, bien que j'y fasse référence dans le lien vers le blog de Jane Street.
cody
1
Mon compilateur C est écrit en C, mais cela ne signifie pas que vous pouvez manipuler l'AST en C ...
BlueRaja - Danny Pflughoeft
2
@immibis: Évidemment, mais si c'est ce qu'il voulait dire, alors cette déclaration est à la fois vide et sans rapport avec la question ...
BlueRaja - Danny Pflughoeft
1

Par exemple, considérons F # (basé sur OCaml). F # n'est pas entièrement homoiconique, mais prend en charge l'obtention du code d'une fonction en tant qu'AST dans certaines circonstances.

En F #, votre printserait représenté comme un Exprqui est imprimé comme:

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

Pour mieux mettre en valeur la structure, voici une autre façon de créer la même chose 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
la source
Je n'ai pas compris cela. Vous voulez dire que F # vous permet de "construire" l'AST d'une expression puis de l'exécuter? Si oui, quelle est la différence avec les langues qui vous permettent d'utiliser la eval(<string>)fonction? ( Selon de nombreuses ressources, avoir une fonction eval est différent d'avoir une homoiconicité - est-ce la raison pour laquelle vous avez dit que F # n'est pas entièrement homoiconique?)
incud
@Ignus Vous pouvez construire vous-même l'AST ou laisser le compilateur le faire. L'homoiconicité "permet d' accéder à tout le code de la langue et de le transformer en données" . En F #, vous pouvez accéder à du code en tant que données. (Par exemple, vous devez marquer printavec l' [<ReflectedDefinition>]attribut.)
svick