J'apprends Scheme du SICP et j'ai l'impression qu'une grande partie de ce qui rend Scheme et, plus encore, LISP spécial est le système macro. Mais, comme les macros sont développées au moment de la compilation, pourquoi les gens ne font-ils pas des systèmes de macros équivalents pour C / Python / Java / quoi que ce soit? Par exemple, on pourrait lier la python
commande à expand-macros | python
ou autre chose. Le code serait toujours portable pour les personnes qui n'utilisent pas le système de macros, il suffit de développer les macros avant de publier du code. Mais je ne connais rien de tel, sauf les modèles en C ++ / Haskell, que je suppose ne sont pas vraiment les mêmes. Qu'en est-il de LISP, le cas échéant, facilite la mise en œuvre de macro-systèmes?
21
Réponses:
De nombreux Lispers vous diront que ce qui rend Lisp spécial, c'est l' homoiconicité , ce qui signifie que la syntaxe du code est représentée en utilisant les mêmes structures de données que les autres données. Par exemple, voici une fonction simple (utilisant la syntaxe Scheme) pour calculer l'hypoténuse d'un triangle rectangle avec les longueurs de côté données:
Maintenant, l'homoiconicité dit que le code ci-dessus est en fait représentable comme une structure de données (en particulier, des listes de listes) dans le code Lisp. Considérez donc les listes suivantes et voyez comment elles se «collent» ensemble:
(define #2# #3#)
(hypot x y)
(sqrt #4#)
(+ #5# #6#)
(square x)
(square y)
Les macros vous permettent de traiter le code source comme cela: des listes de choses. Chacun de ces 6 « sous - listes » contiennent soit des pointeurs vers d' autres listes, ou à des symboles (dans cet exemple:
define
,hypot
,x
,y
,sqrt
,+
,square
).Alors, comment pouvons-nous utiliser l'homoiconicité pour «séparer» la syntaxe et créer des macros? Voici un exemple simple. Réimplémentons la
let
macro, que nous appelleronsmy-let
. Pour rappel,devrait se développer en
Voici une implémentation utilisant des macros Scheme "renommage explicite" † :
Le
form
paramètre est lié à la forme réelle, donc pour notre exemple, il le serait(my-let ((foo 1) (bar 2)) (+ foo bar))
. Voyons donc l'exemple:cadr
saisit la((foo 1) (bar 2))
partie du formulaire.Ensuite, nous récupérons le corps du formulaire.
cddr
saisit la((+ foo bar))
partie du formulaire. (Notez que cela est destiné à récupérer tous les sous-formulaires après la liaison; donc si le formulaire étaitalors le corps serait
((debug foo) (debug bar) (+ foo bar))
.)lambda
expression et l'appel résultants en utilisant les liaisons et le corps que nous avons collectés. Le backtick est appelé une "quasiquote", ce qui signifie de traiter tout ce qui se trouve à l'intérieur de la quasiquote comme des références littérales, à l' exception des bits après les virgules ("unquote").(rename 'lambda)
moyens d'utiliser lalambda
liaison en vigueur lorsque cette macro est définie , plutôt que toutelambda
liaison qui pourrait exister lorsque cette macro est utilisée . (C'est ce qu'on appelle l' hygiène .)(map car bindings)
renvoie(foo bar)
: la première donnée dans chacune des liaisons.(map cadr bindings)
renvoie(1 2)
: la deuxième donnée dans chacune des liaisons.,@
fait le "splicing", qui est utilisé pour les expressions qui retournent une liste: il fait coller les éléments de la liste dans le résultat, plutôt que la liste elle-même.(($lambda (foo bar) (+ foo bar)) 1 2)
, où$lambda
ici se réfère au renommélambda
.Simple, non? ;-) (Si ce n'est pas simple pour vous, imaginez à quel point il serait difficile d'implémenter un système de macro pour d'autres langues.)
Ainsi, vous pouvez avoir des systèmes de macros pour d'autres langues, si vous avez un moyen de pouvoir "séparer" le code source de manière non maladroite. Il y a quelques tentatives. Par exemple, sweet.js le fait pour JavaScript.
† Pour les Schemers chevronnés lisant ceci, j'ai intentionnellement choisi d'utiliser des macros de changement de nom explicites comme compromis moyen entre les
defmacro
s utilisés par d'autres dialectes Lisp etsyntax-rules
(ce qui serait la manière standard d'implémenter une telle macro dans Scheme). Je ne veux pas écrire dans d'autres dialectes lisp, mais je ne veux pas aliéner les non-Schémers qui n'y sont pas habituéssyntax-rules
.Pour référence, voici la
my-let
macro qui utilisesyntax-rules
:La
syntax-case
version correspondante est très similaire:La différence entre les deux est que tout dans
syntax-rules
a un implicite#'
appliqué, donc vous ne pouvez avoir que des paires modèle / modèlesyntax-rules
, donc c'est entièrement déclaratif. En revanche, danssyntax-case
, le bit après le modèle est un code réel qui, en fin de compte, doit renvoyer un objet de syntaxe (#'(...)
), mais peut également contenir un autre code.la source
syntax-rules
, qui est purement déclarative. Pour les macros compliquées, je peux utilisersyntax-case
ce qui est en partie déclaratif et en partie procédural. Et puis il y a le renommage explicite, qui est purement procédural. (La plupart des implémentations de Scheme fourniront l'unsyntax-case
ou l' autre ER. Je n'en ai pas vu un qui fournit les deux. Ils sont équivalents en puissance.)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 untry
bloc, puis appelleraisEndUpdate()
: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 quiMacroStatement
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:
et le faire s'étendre à:
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 lesArguments
etBody
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 deMacroStatement
, et si vous essayez de coder quelque chose qui n'est pas valide pour unMacroStatement
, 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!
la source
if...
ne ressemble pas à un appel de fonction par exemple. Je ne connais pas Boo, mais imaginez que Boo n'avait pas de correspondance de motifs, pourriez-vous l'introduire avec sa propre syntaxe en tant que macro? Mon point est - toute nouvelle macro en Lisp semble 100% naturelle, dans d'autres langues, elles fonctionnent, mais vous pouvez voir les points.Comment? Tout le code dans SICP est écrit dans un style sans macro. Il n'y a pas de macros dans SICP. Ce n'est que dans une note de bas de page à la page 373 que les macros sont mentionnées.
Ils ne le sont pas nécessairement. Lisp fournit des macros dans les interprètes et les compilateurs. Ainsi, il pourrait ne pas y avoir de temps de compilation. Si vous avez un interpréteur Lisp, les macros sont développées au moment de l'exécution. Étant donné que de nombreux systèmes Lisp ont un compilateur intégré, on peut générer du code et le compiler au moment de l'exécution.
Testons cela en utilisant SBCL, une implémentation Common Lisp.
Passons SBCL à l'interpréteur:
Nous définissons maintenant une macro. La macro imprime quelque chose lorsqu'elle est appelée pour du code développé. Le code généré ne s'imprime pas.
Utilisons maintenant la macro:
Voir. Dans le cas ci-dessus, Lisp ne fait rien. La macro n'est pas développée au moment de la définition.
Mais à l'exécution, lorsque le code est utilisé, la macro est développée.
Encore une fois, lors de l'exécution, lorsque le code est utilisé, la macro est développée.
Notez que SBCL ne se développerait qu'une seule fois lors de l'utilisation d'un compilateur. Mais diverses implémentations Lisp fournissent également des interprètes - comme SBCL.
Pourquoi les macros sont-elles faciles en Lisp? Eh bien, ce n'est pas vraiment facile. Seulement en Lisps, et il y en a beaucoup, qui ont un support de macro intégré. Comme beaucoup de Lisps sont livrés avec une machinerie étendue pour les macros, il semble que ce soit facile. Mais les macro-mécanismes peuvent être extrêmement compliqués.
la source
eval
ou leload
code dans n'importe quel langage Lisp, les macros de ceux-ci seront également traitées. Alors que si vous utilisez un système de préprocesseur tel que proposé dans votre question,eval
et similaire ne bénéficiera pas de l'expansion de macro.read
en Lisp. Cette distinction est importante, car elleeval
fonctionne sur les structures de données de la liste (comme mentionné dans ma réponse), et non sur la forme textuelle. Vous pouvez donc utiliser(eval '(+ 1 1))
et récupérer 2, mais si vous(eval "(+ 1 1)")
, vous récupérez"(+ 1 1)"
(la chaîne). Vous utilisezread
pour passer de"(+ 1 1)"
(une chaîne de 7 caractères) à(+ 1 1)
(une liste avec un symbole et deux fixnums).read
heure. Ils fonctionnent au moment de la compilation dans le sens où si vous avez du code comme(and (test1) (test2))
, il sera développé dans(if (test1) (test2) #f)
(dans Scheme) juste une fois, lorsque le code est chargé, plutôt qu'à chaque fois qu'il est exécuté, mais si vous faites quelque chose comme(eval '(and (test1) (test2)))
, qui compilera (et développera macro) cette expression de manière appropriée, au moment de l'exécution.eval
fonctionnent que sur les chaînes de texte, et leurs capacités de modification de syntaxe sont beaucoup plus ternes et / ou encombrantes.L'homoiconicité facilite considérablement la mise en œuvre des macros. L'idée que le code est une donnée et que la donnée est un code permet plus ou moins (sauf capture accidentelle d'identifiants, résolue par des macros hygiéniques ) de se substituer librement l'un à l'autre. Lisp et Scheme rendent cela plus facile avec leur syntaxe d'expressions S qui sont uniformément structurées et donc faciles à transformer en ASTs qui forment la base des macros syntaxiques .
Les langages sans S-Expressions ni homoiconicité rencontreront des problèmes lors de la mise en œuvre des macros syntaxiques, même si cela peut certainement être fait. Le projet Kepler tente par exemple de leur faire découvrir Scala.
Le plus gros problème avec l'utilisation des macros de syntaxe en dehors de la non-homoiconicité, est le problème de la syntaxe générée arbitrairement. Ils offrent une flexibilité et une puissance considérables, mais au prix que votre code source ne soit plus aussi facile à comprendre ou à maintenir.
la source