Macros basiques

Une macro est donc une fonction dont le but est de transformer une expression. La nouvelle expression sera évaluée à la place de la première. Comme exemple simpliste de ce type de transformation, supposons que nous souhaitions pouvoir écrire :

(initialize-rooms rooms)

afin d'obtenir la même chose que si nous avions écrit :

(setf rooms (list (list 'amphi-a 8 18)
                  (list 'amphi-b 8 18)
                  (list 'salle-a 8 18)))

On suppose qu'il s'agit d'une application de gestion de salles, et que rooms est une variable contenant une liste de salles avec leurs heures de disponibilité.

Une macro Common Lisp permettant d'écrire :

(initialize-rooms rooms)

aura le nom initialize-rooms et acceptera un seul paramètre. Ce paramètre n'est pas la valeur de la variable rooms mais le symbole rooms ou en général l'expression qui suit le nom de la macro dans l'appel. Ceci est exactement le contraire de ce qui se passe pour un appel de fonction. La macro aura la structure suivante :

(defmacro initialize-rooms (room-var)
  corps de la macro)

Dans le corps de la macro, le paramètre room-var aura comme valeur, le symbole rooms et la valeur renvoyée par l'appel à la macro, doit être la liste :

(setf rooms (list (list 'amphi-a 8 18)
                  (list 'amphi-b 8 18)
                  (list 'salle-a 8 18)))

Voici une façon d'y arriver :

(defmacro initialize-rooms (room-var)
  (list 'setf
        room-var
        '(list (list 'amphi-a 8 18)
               (list 'amphi-b 8 18)
               (list 'salle-a 8 18))))

On peut d'abord se convaincre que cette macro aura le bon effet :

* (defparameter *rooms1* nil)

*ROOMS1*
* (defparameter *rooms2* nil)

*ROOMS2*
* (setf *rooms1* (list (list 'amphi-a 8 18)
                       (list 'amphi-b 8 18)
                       (list 'salle-a 8 18)))

((AMPHI-A 8 18) (AMPHI-B 8 18) (SALLE-A 8 18))
* (initialize-rooms *rooms2*)

((AMPHI-A 8 18) (AMPHI-B 8 18) (SALLE-A 8 18))
* (equal *rooms1* *rooms2*)

T
*

On peut aussi utiliser la fonction macroexpand-1 pour visualiser l'expansion de l'appel à la macro :

* (macroexpand-1 '(initialize-rooms *rooms2*))

(SETF *ROOMS2*
      (LIST (LIST 'AMPHI-A 8 18)
            (LIST 'AMPHI-B 8 18)
            (LIST 'SALLE-A 8 18)))
T
*

Cette méthode de création de l'expression finale est possible uniquement pour de très petites expressions. La difficulté vient du mélange entre les parties variables (le paramètre room-var) et les parties constantes (le reste).

Pour faciliter la manipulation d'expressions, il existe une fonctionnalité en Common Lisp appelée backquote. Avec cette fonctionnalité, il est possible de créer l'expression finale à partir d'un patron. Son utilisation est similaire à celle de quote. Pour backquote il y a un caractère, l'apostrophe à l'envers, à utiliser qui signifie le début du patron. Une expression à l'intérieur qui commence par le caractère virgule sera évaluée et remplacée par sa valeur. L'exemple ci-dessus devient alors :

(defmacro initialize-rooms (room-var)
  `(setf ,room-var
         (list (list 'amphi-a 8 18)
               (list 'amphi-b 8 18)
               (list 'salle-a 8 18))))

Maintenant, le corps de la macro ressemble à l'expression à renvoyer, et ce sont uniquement les parties précédées du caractère virgule qui seront remplacées par d'autres choses.

En réalité, le caractère backquote (apostrophe à l'envers) est reconnu par read qui est chargé de créer une expression similaire à celle utilisée dans la première version de la macro.

Il est parfois souhaitable de substituer une suite d'expressions dans un patron. C'est le cas lorsque la macro accepte un nombre arbitraire d'arguments. Dans ce cas, il convient d'utiliser le symbole &body dans la lambda-liste de la macro. Bien que synonyme de &rest, le symbole &body est préférable pour l'écriture de macros, car on parle souvent du corps (en Anglais : body) de la macro. Pour substituer une suite d'expressions dans le patron, il n'est pas possible d'utiliser virgule, car on obtient alors une liste des expressions plutôt que la suite elle-même. Pour cette raison, la fonctionnalité backquote autorise aussi la suite des deux caractères ,@.

Voici un exemple d'utilisation de &body et ,@. Il s'agit d'écrire une macro permettant une boucle while similaire à celle du langage C.

(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

Voilà comment s'en servir :

* (defparameter *i* 0)

*I*
* (while (< *i* 5)
    (print *i*)
    (incf *i*))

0
1
2
3
4
NIL
*

On peut vérifier l'expansion de cet appel :

* (macroexpand-1 '(while (< *i* 5)
                    (print *i*)
                    (incf *i*)))

(DO () ((NOT (< *I* 5))) (PRINT *I*) (INCF *I*))
T
*

Indentée différemment, cette expression peut s'écrire :

(DO ()
    ((NOT (< *I* 5)))
  (PRINT *I*)
  (INCF *I*))

On voit alors que ,@body a été remplacé par une suite de deux expressions, ce qui n'aurait pas été possible avec virgule seulement.

Irene DURAND
2011-10-14