Capture de variables

Supposons que nous souhaitions créer une macro dovec pour évaluer une suite d'expressions pour chaque élément d'un vecteur. Une telle macro peut être utilisée de la manière suivante :

* (defparameter *v* #(1 2 3 4))

*V*
* (dovec elem *v*
    (print elem))

1
2
3
4
NIL
*

Voici une tentative d'écriture d'une telle macro :

(defmacro dovec (elem-var vector &body body)
  `(do* ((i 0 (1+ i)))
        ((>= i (length ,vector)))
     (let ((,elem-var (aref ,vector i)))
       ,@body)))

La documentation de cette macro pourrait être : ``cette macro accepte une variable, une expression dont la valeur doit être un vecteur et une suite d'expressions qui seront évaluées avec la variable ayant comme valeur chaque élément du vecteur.''

Mais il y a un problème : la macro ne s'exécute pas toujours conformément à la documentation. Voici d'abord un exemple qui marche :

* (defparameter *v* #(1 2 3 4))

*V*
* (let ((x 10))
    (print x)
    (dovec elem *v*
      (print (+ elem x))))

10
11
12
13
14
NIL
*

Voici maintenant un exemple similaire, mais qui ne marche pas :

* (defparameter *v* #(1 2 3 4))

*V*
* (let ((i 10))
    (print i)
    (dovec elem *v*
      (print (+ elem i))))

10
1
3
5
7
NIL
*

L'utilisateur de la macro s'attend probablement à ce que ces deux exemples donnent le même résultat. Après tout, la seule différence est le nom de la variable locale : x dans le premier exemple et i dans le deuxième. Pourtant les deux résultats sont complètement différents. Étant donnée la documentation, l'utilisateur s'attend probablement au résultat du premier exemple et pas à celui du deuxième.

Le second exemple échoue du fait qu'il existe deux variables différentes dont le nom est i. Le second exemple est équivalent à :

(let ((i 10))
  (print i)
  (do* ((i 0 (1+ i)))
       ((>= i (length *v*)))
    (let ((elem (aref *v* i)))
      (print (+ elem i)))))

Mais la boucle do* introduit une deuxième variable i. Si l'on met cette deuxième variable en majuscule, on obtient :

(let ((i 10))
  (print i)
  (do* ((I 0 (1+ I)))
       ((>= I (length *v*)))
    (let ((elem (aref *v* I)))
      (print (+ elem I)))))

Le problème est que dans l'expression

(print (+ elem I))
c'est le I majuscule qui est utilisé et non (comme le pensait l'utilisateur) le i minuscule. Ce phénomène est appelé capture de variable et c'est le plus souvent (mais pas toujours) un phénomène indésirable.

Il est bien sûr possible de modifier la documentation pour qu'elle soit conforme au comportement de la macro en rajoutant : ``Attention : ne pas utiliser la variable i dans les expressions de la suite.'' Mais c'est une solution peu satisfaisante.

La bonne solution est d'utiliser la possibilité en Common Lisp de créer des symboles garantis différents de tout autre symbole utilisé dans le programme. La fonction gensym renvoie à chaque appel un nouveau symbole qui ne peut pas être confondu avec un symbole explicitement tapé par le programmeur. Voici comment modifier l'exemple pour éviter la capture :

(defmacro dovec (elem-var vector &body body)
  (let ((var (gensym)))
    `(do* ((,var 0 (1+ ,var)))
          ((>= ,var (length ,vector)))
       (let ((,elem-var (aref ,vector ,var)))
         ,@body))))

Cet exemple illustre aussi la possibilité d'effectuer un calcul préalable à la création de l'expansion. Ici, la partie :

(let ((var (gensym)))

est exécutée par le compilateur pendant la macro-expansion. L'effet de cette expression est de créer un nouveau symbole. Ce symbole sera la valeur de la variable locale var qui n'existe que pendant la macro-expansion. Dans le patron, la variable var est précédée de virgule, ce qui signifie qu'elle sera évaluée. Chaque occurrence de var dans le patron sera donc remplacée par le symbole généré par gensym. Ce symbole servira donc de variable dans l'expansion finale. Il est important de réaliser qu'une nouvelle variable est générée à chaque fois que la macro est appelée. De cette façon, il est possible d'imbriquer des appels à la macro sans risque de capture.

Avec cette nouvelle définition, le deuxième exemple marche aussi :

* (let ((i 10))
    (print i)
    (dovec elem *v*
      (print (+ elem i))))

10
11
12
13
14
NIL
*

Irene DURAND
2011-10-14