Une opinion dissidente: l'homoiconicité de Lisp est beaucoup moins utile que la plupart des fans de Lisp ne le croient.
Pour comprendre les macros syntaxiques, il est important de comprendre les compilateurs. Le travail d'un compilateur est de transformer du code lisible par l'homme en code exécutable. D'un point de vue de très haut niveau, cela comporte deux phases globales: l' analyse et la génération de code .
L'analyse est le processus de lecture du code, de son interprétation selon un ensemble de règles formelles et de sa transformation en une structure arborescente, généralement connue sous le nom d'AST (Abstract Syntax Tree). Malgré toute la diversité des langages de programmation, il s'agit d'un point commun remarquable: essentiellement chaque langage de programmation à usage général est analysé dans une arborescence.
La génération de code prend l'AST de l'analyseur comme entrée et le transforme en code exécutable via l'application de règles formelles. Du point de vue des performances, il s'agit d'une tâche beaucoup plus simple; de nombreux compilateurs de langage de haut niveau consacrent 75% ou plus de leur temps à l'analyse.
La chose à retenir à propos de Lisp est que c'est très, très vieux. Parmi les langages de programmation, seul FORTRAN est plus ancien que Lisp. Il y a bien longtemps, l'analyse (la partie lente de la compilation) était considérée comme un art sombre et mystérieux. Les articles originaux de John McCarthy sur la théorie du Lisp (à l'époque où ce n'était qu'une idée qu'il n'aurait jamais pensé pouvoir être implémenté comme un véritable langage de programmation informatique) décrivent une syntaxe un peu plus complexe et expressive que les expressions S modernes partout pour tout. "notation. Cela est arrivé plus tard, lorsque les gens essayaient de le mettre en œuvre. Parce que l'analyse syntaxique n'était pas bien comprise à l'époque, ils ont fondamentalement punté dessus et abrégé la syntaxe dans une structure arborescente homoiconique afin de rendre le travail de l'analyseur totalement trivial. Le résultat final est que vous (le développeur) devez faire beaucoup de l'analyseur » s y travailler en écrivant l'AST formel directement dans votre code. L'homoiconicité "ne rend pas les macros tellement plus faciles" qu'elle rend l'écriture de tout le reste beaucoup plus difficile!
Le problème avec cela est que, en particulier avec le typage dynamique, il est très difficile pour les expressions S de transporter beaucoup d'informations sémantiques avec elles. Lorsque toute votre syntaxe est le même type de chose (listes de listes), il n'y a pas beaucoup de contexte fourni par la syntaxe, et donc le système de macros a très peu de choses à travailler.
La théorie du compilateur a parcouru un long chemin depuis les années 1960, lorsque Lisp a été inventé, et bien que les choses qu'il ait accomplies étaient impressionnantes à l'époque, elles semblent plutôt primitives maintenant. Pour un exemple d'un système de métaprogrammation moderne, jetez un œil à la langue Boo (malheureusement sous-estimée). Boo est typé statiquement, orienté objet et open source, donc chaque nœud AST a un type avec une structure bien définie dans laquelle un développeur de macros peut lire le code. Le langage a une syntaxe relativement simple inspirée de Python, avec divers mots clés qui donnent une signification sémantique intrinsèque aux structures arborescentes construites à partir d'eux, et sa métaprogrammation a une syntaxe de quasiquote intuitive pour simplifier la création de nouveaux nœuds AST.
Voici une macro que j'ai créée hier lorsque j'ai réalisé que j'appliquais le même modèle à un tas d'endroits différents dans le code GUI, où j'appellerais BeginUpdate()
un contrôle d'interface utilisateur, effectuerais une mise à jour dans un try
bloc, puis appellerais EndUpdate()
:
macro UIUpdate(value as Expression):
return [|
$value.BeginUpdate()
try:
$(UIUpdate.Body)
ensure:
$value.EndUpdate()
|]
La macro
commande est, en fait, une macro elle - même , qui prend un corps de macro en entrée et génère une classe pour traiter la macro. Il utilise le nom de la macro comme variable qui MacroStatement
remplace le nœud AST qui représente l'invocation de macro. Le [| ... |] est un bloc de quasiquote, générant l'AST qui correspond au code à l'intérieur et à l'intérieur du bloc de quasiquote, le symbole $ fournit la fonction "unquote", se substituant dans un nœud comme spécifié.
Avec cela, il est possible d'écrire:
UIUpdate myComboBox:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
et le faire s'étendre à:
myComboBox.BeginUpdate()
try:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
ensure:
myComboBox.EndUpdate()
Exprimant la macro de cette façon est plus simple et plus intuitive que ce serait dans une macro Lisp, parce que le développeur connaît la structure MacroStatement
et sait comment les Arguments
et Body
propriétés de travail, et que la connaissance inhérente peut être utilisé pour exprimer les concepts impliqués dans une très intuitive façon. Il est également plus sûr, car le compilateur connaît la structure de MacroStatement
, et si vous essayez de coder quelque chose qui n'est pas valide pour un MacroStatement
, le compilateur le détectera immédiatement et signalera l'erreur au lieu que vous ne le sachiez jusqu'à ce que quelque chose vous explose à Durée.
Greffer des macros sur Haskell, Python, Java, Scala, etc. n'est pas difficile car ces langages ne sont pas homoiconiques; c'est difficile parce que les langues ne sont pas conçues pour eux, et cela fonctionne mieux lorsque la hiérarchie AST de votre langue est conçue à partir de zéro pour être examinée et manipulée par un macro système. Lorsque vous travaillez avec un langage conçu avec la métaprogrammation à l'esprit dès le début, les macros sont beaucoup plus simples et plus faciles à utiliser!