Recherche…


Remarques

Le but des macros

Les macros sont destinées à générer du code, à transformer du code et à fournir de nouvelles notations. Ces nouvelles notations peuvent être plus adaptées pour mieux exprimer le programme, par exemple en fournissant des constructions au niveau du domaine ou de nouveaux langages intégrés.

Les macros peuvent rendre le code source plus explicite, mais le débogage peut être rendu plus difficile. En règle générale, il ne faut pas utiliser de macros quand une fonction normale fera l'affaire. Lorsque vous les utilisez, évitez les pièges habituels, essayez de vous en tenir aux schémas et conventions de dénomination couramment utilisés.

Commande Macroexpansion

Par rapport aux fonctions, les macros sont développées dans l'ordre inverse. le plus en premier, le dernier en dernier. Cela signifie que, par défaut, il est impossible d'utiliser une macro interne pour générer la syntaxe requise pour une macro externe.

Ordre d'évaluation

Parfois, les macros doivent déplacer les formulaires fournis par l'utilisateur. Il faut s'assurer de ne pas changer l'ordre dans lequel ils sont évalués. L'utilisateur peut se fier aux effets secondaires se produisant dans l'ordre.

Évaluer une seule fois

L'expansion d'une macro doit souvent utiliser plusieurs fois la même valeur fournie par l'utilisateur. Il est possible que le formulaire ait des effets secondaires ou qu’il appelle une fonction coûteuse. Ainsi, la macro doit s’assurer de n’évaluer que de telles formes une seule fois. Cela se fera généralement en assignant la valeur à une variable locale (dont le nom est GENSYM ed).

Fonctions utilisées par les macros, en utilisant EVAL-WHEN

Les macros complexes comportent souvent des parties de leur logique implémentées dans des fonctions distinctes. Il ne faut cependant pas oublier que les macros sont développées avant la compilation du code. Lors de la compilation d'un fichier, les fonctions et les variables définies dans le même fichier ne seront pas disponibles par défaut lors de l'exécution de la macro. Toutes les définitions de fonctions et de variables, dans le même fichier, utilisées par une macro doivent être placées dans un EVAL-WHEN . EVAL-WHEN doit avoir toutes les trois fois spécifié, lorsque le code joint doit également être évalué pendant le chargement et le runtime.

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun foobar () ...))

Cela ne s'applique pas aux fonctions appelées à partir de l'expansion de la macro, seulement celles appelées par la macro elle-même.

Modèles Macro communs

TODO: Peut-être déplacer les explications à des remarques et ajouter des exemples séparément

FOOF

Dans Common Lisp, il existe un concept de références généralisées . Ils permettent à un programmeur de définir des valeurs à différents "endroits" comme s'ils étaient des variables. Les macros qui utilisent cette capacité ont souvent un préfixe F dans le nom. Le lieu est généralement le premier argument de la macro.

Exemples de la norme: INCF , DECF , ROTATEF , SHIFTF , REMF .

Un exemple idiot, une macro qui retourne le signe d'un magasin de nombres dans un lieu:

(defmacro flipf (place)
  `(setf ,place (- ,place)))

AVEC-FOO

Les macros qui acquièrent et libèrent en toute sécurité une ressource sont généralement nommées avec un WITH- WITH-. La macro doit généralement utiliser une syntaxe telle que:

(with-foo (variable details-of-the-foo...)
  body...)

Exemples tirés du standard: WITH-OPEN-FILE , WITH-OPEN-STREAM , WITH-INPUT-FROM-STRING , WITH-OUTPUT-TO-STRING .

Une approche pour implémenter ce type de macro qui peut éviter certains des pièges de la pollution de nom et de l’évaluation multiple involontaire consiste à implémenter une version fonctionnelle en premier. Par exemple, la première étape de l'implémentation d'une macro with-widget qui crée en toute sécurité un widget et le nettoie peut être une fonction:

(defun call-with-widget (args function)
  (let ((widget (apply #'make-widget args))) ; obtain WIDGET
    (unwind-protect (funcall function widget)  ; call FUNCTION with WIDGET
      (cleanup widget)                         ; cleanup

Comme il s'agit d'une fonction, l'étendue des noms au sein de la fonction ou du fournisseur ne pose aucun problème et facilite l'écriture d'une macro correspondante:

(defmacro with-widget ((var &rest args) &body body)
  `(call-with-widget (list ,@args) (lambda (,var) ,@body)))

DO-FOO

Les macros qui parcourent quelque chose sont souvent nommées avec un préfixe DO . La macro-syntaxe devrait généralement être sous la forme

(do-foo (variable the-foo-being-done return-value)
  body...)

Exemples de la norme: DOTIMES , DOLIST , DO-SYMBOLS .

FOOCASE, EFOOCASE, CFOOCASE

Les macros qui correspondent à une entrée par rapport à certains cas sont souvent nommées avec un correctif de type CASE . Il y a souvent une variable E...CASE , qui signale une erreur si l'entrée ne correspond à aucun des cas, et C...CASE , qui signale une erreur continue. Ils devraient avoir une syntaxe comme

(foocase input
  (case-to-match-against (optionally-some-params-for-the-case)
   case-body-forms...)
  more-cases...
  [(otherwise otherwise-body)])

Exemples tirés de la norme: CASE , TYPECASE , HANDLER-CASE .

Par exemple, une macro qui associe une chaîne à des expressions régulières et lie les groupes de registres à des variables. Utilise CL-PPCRE pour les expressions régulières.

(defmacro regexcase (input &body cases)
  (let ((block-sym (gensym "block"))
        (input-sym (gensym "input")))
    `(let ((,input-sym ,input))
       (block ,block-sym
         ,@(loop for (regex vars . body) in cases
                 if (eql regex 'otherwise)
                   collect `(return-from ,block-sym (progn ,vars ,@body))
                 else
                   collect `(cl-ppcre:register-groups-bind ,vars
                                (,regex ,input-sym)
                              (return-from ,block-sym
                                (progn ,@body))))))))

(defun test (input)
  (regexcase input
    ("(\\d+)-(\\d+)" (foo bar)
      (format t "Foo: ~a, Bar: ~a~%" foo bar))
    ("Foo: (\\w+)$" (foo)
      (format t "Foo: ~a.~%" foo))
    (otherwise (format t "Didn't match.~%"))))

(test "asd 23-234 qwe")
; Foo: 23, Bar: 234
(test "Foo: Foobar")
; Foo: Foobar.
(test "Foo: 43 - 23")
; Didn't match.

DEFINE-FOO, DEFFOO

Les macros qui définissent des choses sont généralement nommées avec un DEFINE- ou DEF .

Exemples de la norme: DEFUN , DEFMACRO , DEFINE-CONDITION .

Macros anaphoriques

Une macro anaphorique est une macro qui introduit une variable (souvent IT ) qui capture le résultat d'un formulaire fourni par l'utilisateur. Un exemple courant est le If anaphorique, qui est comme un IF régulier, mais définit également la variable IT pour faire référence au résultat du formulaire de test.

(defmacro aif (test-form then-form &optional else-form)
  `(let ((it ,test-form))
     (if it ,then-form ,else-form)))

(defun test (property plist)
  (aif (getf plist property)
       (format t "The value of ~s is ~a.~%" property it)
       (format t "~s wasn't in ~s!~%" property plist)))

(test :a '(:a 10 :b 20 :c 30))
; The value of :A is 10.
(test :d '(:a 10 :b 20 :c 30))
; :D wasn't in (:A 10 :B 20 :C 30)!

MACROEXPAND

L'expansion des macros est le processus consistant à transformer les macros en code réel. Cela se produit généralement dans le cadre du processus de compilation. Le compilateur développera toutes les formes de macro avant de compiler le code. L'expansion de la macro se produit également lors de l' interprétation du code Lisp.

On peut appeler MACROEXPAND manuellement pour voir comment se développe une forme de macro.

CL-USER> (macroexpand '(with-open-file (file "foo")
                        (do-something-with file)))
(LET ((FILE (OPEN "foo")) (#:G725 T))
  (UNWIND-PROTECT
      (MULTIPLE-VALUE-PROG1 (PROGN (DO-SOMETHING-WITH FILE)) (SETQ #:G725 NIL))
    (WHEN FILE (CLOSE FILE :ABORT #:G725))))

MACROEXPAND-1 est identique, mais ne s’étend qu’une seule fois. Ceci est utile lorsque vous essayez de donner un sens à une forme de macro qui se développe en une autre forme de macro.

CL-USER> (macroexpand-1 '(with-open-file (file "foo")
                          (do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))

Notez que ni MACROEXPAND ni MACROEXPAND-1 développent le code Lisp à tous les niveaux. Ils développent uniquement la forme de macro de niveau supérieur. Pour macroexpand une forme complète à tous les niveaux, il faut un code walker pour le faire. Cette fonctionnalité n'est pas fournie dans le standard Common Lisp.

Backquote - écriture de modèles de code pour les macros

Code de retour des macros. Comme le code dans Lisp est constitué de listes, on peut utiliser les fonctions de manipulation de liste régulières pour le générer.

;; A pointless macro
(defmacro echo (form)
  (list 'progn
        (list 'format t "Form: ~a~%" (list 'quote form))
        form))

C'est souvent très difficile à lire, en particulier dans les macros plus longues. La macro lecteur Backquote permet d'écrire des modèles cités remplis en évaluant sélectivement des éléments.

(defmacro echo (form)
  `(progn
     (format t "Form: ~a~%" ',form)
     ,form))

(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))

Cette version ressemble presque à un code normal. Les virgules sont utilisées pour évaluer FORM ; tout le reste est retourné tel quel. Notez que dans ',form le guillemet simple est en dehors de la virgule, donc il sera retourné.

On peut aussi utiliser ,@ pour épisser une liste dans la position.

(defmacro echo (&rest forms)
  `(progn
     ,@(loop for form in forms collect `(format t "Form: ~a~%" ,form))
     ,@forms))

(macroexpand '(echo (+ 3 4) 
                    (print "foo")
                    (random 10)))
;=> (PROGN
;    (FORMAT T "Form: ~a~%" (+ 3 4))
;    (FORMAT T "Form: ~a~%" (PRINT "foo"))
;    (FORMAT T "Form: ~a~%" (RANDOM 10))
;    (+ 3 4)
;    (PRINT "foo")
;    (RANDOM 10))

Backquote peut également être utilisé en dehors des macros.

Symboles uniques pour empêcher les conflits de noms dans les macros

L'expansion d'une macro doit souvent utiliser des symboles qui n'ont pas été passés en tant qu'arguments par l'utilisateur (en tant que noms de variables locales, par exemple). Il faut s'assurer que ces symboles ne peuvent pas entrer en conflit avec un symbole que l'utilisateur utilise dans le code environnant.

Ceci est généralement réalisé en utilisant GENSYM , une fonction qui retourne un nouveau symbole.

Mal

Considérons la macro ci-dessous. Il crée une DOTIMES DOTIMES qui collecte également le résultat du corps dans une liste qui est renvoyée à la fin.

(defmacro dotimes+collect ((var count) &body body)
  `(let ((result (list)))
     (dotimes (,var ,count (nreverse result))
       (push (progn ,@body) result))))

(dotimes+collect (i 5)
  (format t "~a~%" i)
  (* i i))
; 0
; 1
; 2
; 3
; 4
;=> (0 1 4 9 16)

Cela semble fonctionner dans ce cas, mais si l'utilisateur avait un nom de variable RESULT , qu'il utilise dans le corps, les résultats ne seraient probablement pas ceux attendus par l'utilisateur. Considérez cette tentative d'écrire une fonction qui collecte une liste de sommes de tous les entiers jusqu'à N :

(defun sums-upto (n)
  (let ((result 0))
    (dotimes+collect (i n)
      (incf result i))))

(sums-upto 10) ;=> Error!

Bien

Pour résoudre le problème, nous devons utiliser GENSYM pour générer un nom unique pour la variable RESULT dans la macro.

(defmacro dotimes+collect ((var count) &body body)
  (let ((result-symbol (gensym "RESULT")))
    `(let ((,result-symbol (list)))
       (dotimes (,var ,count (nreverse ,result-symbol))
         (push (progn ,@body) ,result-symbol)))))

(sums-upto 10) ;=> (0 1 3 6 10 15 21 28 36 45)

TODO: Comment faire des symboles à partir de chaînes

TODO: éviter les problèmes avec les symboles dans différents paquets

si-laisser, quand-laisser, -let macros

Ces macros fusionnent le flux de contrôle et la liaison. Ils constituent une amélioration par rapport aux macros anaphoriques anaphoriques car ils permettent au développeur de communiquer le sens par le biais du nommage. En tant que tel, leur utilisation est recommandée par rapport à leurs homologues anaphoriques.

(if-let (user (get-user user-id))
  (show-dashboard user)
  (redirect 'login-page))

FOO-LET macros FOO-LET lient une ou plusieurs variables, puis utilisent ces variables comme formulaire de test pour le conditionnel correspondant ( IF , WHEN ). Plusieurs variables sont combinées avec AND . La branche choisie est exécutée avec les liaisons en vigueur. Une simple implémentation d'une variable de IF-LET pourrait ressembler à ceci:

(defmacro if-let ((var test-form) then-form &optional else-form)
  `(let ((,var ,test-form))
     (if ,var ,then-form ,else-form)))

(macroexpand '(if-let (a (getf '(:a 10 :b 20 :c 30) :a))
               (format t "A: ~a~%" a)
               (format t "Not found.~%")))
; (LET ((A (GETF '(:A 10 :B 20 :C 30) :A)))
;   (IF A
;       (FORMAT T "A: ~a~%" A)
;       (FORMAT T "Not found.~%")))

Une version prenant en charge plusieurs variables est disponible dans la bibliothèque Alexandria .

Utilisation de macros pour définir des structures de données

Une utilisation courante des macros consiste à créer des modèles pour des structures de données qui obéissent à des règles communes mais peuvent contenir des champs différents. En écrivant une macro, vous pouvez autoriser la configuration détaillée de la structure de données sans avoir à répéter le code standard, ni utiliser une structure moins efficace (comme un hachage) en mémoire uniquement pour simplifier la programmation.

Par exemple, supposons que nous souhaitons définir un certain nombre de classes ayant une gamme de propriétés différentes, chacune avec un getter et un setter. De plus, pour certaines (mais pas toutes) de ces propriétés, nous souhaitons que le compositeur appelle une méthode sur l’objet lui indiquant que la propriété a été modifiée. Bien que Common LISP ait déjà un raccourci pour écrire des getters et des setters, écrire un setter personnalisé standard de cette manière nécessiterait normalement de dupliquer le code qui appelle la méthode de notification dans chaque setter, ce qui pourrait être compliqué . Cependant, en définissant une macro, cela devient beaucoup plus facile:

(defmacro notifier (class slot) 
  "Defines a setf method in (class) for (slot) which calls the object's changed method."
   `(defmethod (setf ,slot) (val (item ,class))
     (setf (slot-value item ',slot) val)
     (changed item ',slot)))

(defmacro notifiers (class slots)
  "Defines setf methods in (class) for all of (slots) which call the object's changed method."
  `(progn 
     ,@(loop for s in slots collecting `(notifier ,class ,s))))

(defmacro defclass-notifier-slots (class nslots slots)  
  "Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
  `(progn
     (defclass ,class () 
       ( ,@(loop for s in nslots collecting `(,s :reader ,s)) 
         ,@(loop for s in slots collecting `(,s :accessor ,s))))   
     (notifiers ,class ,nslots)))

Nous pouvons maintenant écrire (defclass-notifier-slots foo (bar baz qux) (waldo)) et définir immédiatement une classe foo avec un slot waldo (créé par la seconde partie de la macro avec la spécification (waldo :accessor waldo) ) , et slots bar , baz et qux avec les setters qui appellent la méthode changed (où le getter est défini par la première partie de la macro, (bar :reader bar) et le setter par la macro notifier invoquée).

En plus de nous permettre de définir rapidement plusieurs classes qui se comportent de cette façon, avec un grand nombre de propriétés, sans répétition, nous avons l’avantage habituel de réutiliser le code: si nous décidons plus tard de changer macro, et la structure de chaque classe qui l'utilise changera.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow