Ordinateurs

Clojure : macros d’écriture de macros – InformatiKKa

Si vous écrivez une fonction et qu’elle devient assez grosse pour être maladroite, je suis sûr que vous savez comment la décomposer en plusieurs fonctions plus petites, chacune étant compréhensible. Bien sûr, c’est une bonne pratique dans n’importe quel langage, mais c’est particulièrement simple et puissant dans les langages fonctionnels, où vous n’avez pas à vous soucier des effets secondaires : s’il y a un morceau de code que vous voulez diviser, vous pouvez déplacer sans douleur ce code mot pour mot dans une nouvelle fonction, en prenant comme paramètres les paramètres locaux sur lesquels il doit fonctionner. La partie la plus difficile de tout le processus consiste à trouver un nom significatif pour la nouvelle fonction d’assistance !

Les macros peuvent développer des problèmes similaires si elles grossissent suffisamment, alors naturellement vous voulez aussi les diviser, n’est-ce pas ? Découpez simplement une partie de la macro trop grande, collez-la dans une nouvelle macro et vous êtes prêt.

(defmacro with-magic
  [magic-fn log-string & body]
  (let [magic-sym (gensym "magic-")]
    `(let [~magic-sym (fn [arg#]
                        (println ~log-string arg#)
                        (~magic-fn arg#))]
       ~@(postwalk-replace {magic-fn magic-sym} body))))

(macroexpand '(with-magic ! "OMG doing magic with" 
                (let [x 1] (! "test"))))

(let* [magic-2968 (clojure.core/fn
                     [arg__2958__auto__]
                     (clojure.core/println "OMG doing magic with" arg__2958__auto__) 
                     (! arg__2958__auto__))] 
   (let [x 1] 
     (magic-2968 "test")))

Peut-être une macro stupide, mais pas totalement triviale à écrire. Il prend un symbole à rechercher et remplace toutes les instances de ce symbole par une fonction spécialement construite qui imprime quelque chose avant de faire le vrai travail. Supposons donc que nous décidions que cette macro devient suffisamment compliquée pour que nous souhaitions la scinder – la manière logique de le faire semble être de créer une macro distincte qui crée ces fonctions « magic-xxx ».

(defmacro make-magic-fn [magic-fn log-string]
  `(fn [arg#]
     (println ~log-string arg#)
     (~magic-fn arg#)))

(defmacro with-magic
  [magic-fn log-string & body]
  (let [magic-sym (gensym "magic-")]
    `(let [~magic-sym ~(make-magic-fn magic-fn log-string)]
       ~@(postwalk-replace {magic-fn magic-sym} body))))

(macroexpand '(with-magic ! "OMG doing magic with" 
                (let [x 1] (! "test"))))

(let* [magic-3340 #] 
   (let [x 1] (magic-3340 "test")))

Ça n’a pas l’air bien du tout ! Au lieu du gentil (fn…) forme que nous avons obtenue de la version originale, nous obtenons ce brut # chose, qui ne sera certainement pas un code légal si vous essayez d’utiliser la macro (plutôt que de simplement l’étendre comme je l’ai fait ici). Mais qu’est-ce qui n’allait pas ? make-magic-fn n’est qu’un extrait de la macro with-magic originale, alors les résultats ne devraient-ils pas être identiques ?

Les macros ne sont que des fonctions

Le problème est qu’en réalité, les macros ne sont que des fonctions avec deux propriétés spéciales :

  1. Leurs arguments ne sont pas évalués
  2. Leurs valeurs de retour sont développées sur place et traitées comme du code

C’est ça! Le backtick fournit un raccourci utile pour le cas d’utilisation le plus courant, qui est essentiellement un modèle de code dans lequel vous assemblez les arguments de l’utilisateur aux endroits appropriés ; mais vous devez vous rappeler ce qui se passe réellement dans les coulisses. Votre macro reçoit comme arguments un certain nombre de symboles et de listes, et renvoie une liste.

Ici, nous voulons que les arguments de notre fonction d’assistance soient évalués : nous ne voulons pas que la fonction retournée contienne les symboles « magic-fn » ou « log-string », nous voulons qu’elle contienne les valeurs de ces reliures. Et nous ne voulons pas que le résultat de (make-magic-fn) soit développé en place et interprété comme du code dans notre macronous voulons juste prendre la liste renvoyée par make-magic-fn et la placer dans la liste que with-magic renverra éventuellement.

Donc, ce que nous recherchons réellement, c’est quelque chose comme une macro, mais sans les propriétés 1 et 2 ci-dessus. devine quoi? C’est juste une fonction ! Ce que nous essayons vraiment d’écrire est une fonction qui permet de générer facilement des listes qui ressemblent à ‘(fn […] (…)), et utilisez cette fonction depuis notre macro. Étant donné que les macros disposent de toute la puissance du langage au moment de la compilation, nous pouvons simplement créer ceci en tant que fonction que nous appelons au moment de la compilation.

Et dans ce cas, c’est tout ce qu’il faut ! Si vous remplacez (defmacro make-magic-fn) par (defn make-magic-fn), tout fonctionne comme un charme. Vous pouvez continuer à utiliser le raccourci ` à partir de la version fonctionnelle de make-magic-fn : ` n’est pas un outil magique qui ne fonctionne qu’à l’intérieur des macros, c’est un raccourci pratique pour générer des listes dont seule une partie du contenu est citée.

Ne vous portez pas volontaire pour une jambe de bois

Les macros imbriquées sont comme des amputations : elles sont très douloureuses et il existe généralement une meilleure solution, mais en de rares occasions, vous n’avez pas le choix. Ainsi, alors que l’indice ci-dessus sur l’aide à l’écriture les fonctions au lieu d’aide macros suffira généralement à vous éviter des ennuis, les astuces d’écriture de macros imbriquées sont toujours une bonne chose à connaître.

Par exemple, supposons que vous vouliez écrire un tas de macros très similaires, comme (avec-explosions), (avec-effets-spéciaux), etc. Peut-être qu’ils ressemblent tous à ça mais avec des noms différents et des formes interposées différentes.

(defmacro with-explosions [& body]
  `(do
     ~@(interpose `(println "BOOM") body)))

Au lieu d’écrire cette macro des centaines de fois, vous décidez judicieusement d’écrire une macro qui prend une série de paires (nom, interposition) et écrit une macro pour chacune d’elles. L’astuce consistera à obtenir les formes imbriquées entre guillemets, sans guillemets et gensym #, je vais donc simplement montrer la solution ici, puis discuter de la raison pour laquelle elle ressemble à cela.

(defmacro build-movie-set [& scenes]
  (let [name-vals (partition 2 scenes)]
    `(do
       ~@(for [[name val] name-vals]
           `(defmacro ~(symbol (str "with-" name "s"))
              ([~'& body#]
                 `(do
                    ~@(interpose `(println ~~val)
                                 body#))))))))

Puis-je vous citer là-dessus ?

Cela a vraiment l’air dégoûtant. Je compte quatre formes de backtick, qui sont diversement non citées dans la métamacro. Les choses les plus intéressantes que vous verrez ici sont ~~val et [~’& body#].

Le premier d’entre eux est dû au fait que le contexte dans lequel val est utilisé est à deux niveaux de backtick du contexte dans lequel il a une valeur, nous devons donc le retirer deux fois.

Le second existe parce que, comme vous le savez, l’espace de noms de l’opérateur backtick qualifie tous les symboles qu’il voit… et & est un symbole légal ! Ainsi, backtick essaiera de le remplacer par user/& ou movies/& ou quelque chose ; puis quand defmacro voit cela, il ne se rendra pas compte que vous vouliez et corps. La solution consiste à quitter la citation de syntaxe (avec ~) et à entrer une citation réelle et littérale (avec ‘). Cela vous laissera sans encombre.

Faites défiler pour continuer

Il existe des cas similaires où vous pourriez avoir besoin d’accéder à un symbole de gensym à partir d’une couche de macro moins profonde : vous pouvez accéder à ceux avec ~foo# – c’est-à-dire « ne citez pas foo# dans cette forme de backtick imbriquée, Je veux la valeur réelle de foo# ». Vous devrez peut-être également citer quelque chose dans le contexte développé, mais pas dans le contexte d’expansion : c’est ‘~foo. Vous pouvez empiler ces sortes de choses les unes sur les autres aussi longtemps que vous le souhaitez ; voici un exemple que vous pouvez utiliser pour améliorer le support IDE de vos macros générées automatiquement :

(defmacro whatever [name & args]
  `(defn ^{:arglists '~'([data])} ~name
     ([data#]
       (do something with data# and ~args))))

Avec une grande puissance…

Les macros de Clojure vous permettent d’automatiser l’écriture de code, en plus d’écrire du code qui automatise les choses. C’est une fonctionnalité puissante, mais elle comporte de nombreuses arêtes vives auxquelles vous devez faire attention. En particulier, réfléchissez-y à deux fois avant d’essayer d’imbriquer des macros : c’est généralement la mauvaise réponse. Mais quand c’est bon, j’espère que les conseils ci-dessus vous faciliteront le processus.

Cet article est exact et fidèle au meilleur de la connaissance de l’auteur. Le contenu est uniquement à des fins d’information ou de divertissement et ne remplace pas un conseil personnel ou un conseil professionnel en matière commerciale, financière, juridique ou technique.

© 2011 amalloy

Matthieu Molloy le 29 octobre 2014 :

merci amalloy,

Une bonne référence pour une lecture plus approfondie est la page wikibooks sur les macros de lecteur http://en.wikibooks.org/wiki/Learning_Clojure/Read…

Alex Coventry le 02 août 2013 :

Votre dernier exemple ne fonctionne pas tout à fait, car « ^ » est une macro de lecteur.

http://stackoverflow.com/questions/7754429/clojure…

attrape-poulpe le 13 janvier 2012 :

Il semble que le fait de devoir compter les arguments passés dans une macro serait un cas où une fonction intégrée résoudrait le problème. Est-ce une hypothèse juste?

Nicolas Buduroïc le 20 avril 2011 :

Super article, ça m’a épargné un gros mal de tête ! De plus, si vous devez utiliser la macro interne &form var, vous devrez la supprimer deux fois :

(déf macro fou [name & args]

(Remarque [name* (symbol (str name \*))]

`(faire

(defn ~nom* [self# & params#]

(prn self#))

(def macro ~nom [~’& args#]

`(~~nom* (guillemets ~~’&form) ~@args#)))))

A lire aussi :  Comment protéger par mot de passe un dossier sous Linux Ubuntu
Bouton retour en haut de la page