Reprenons l'exemple de la section précédente :
(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))))
C'est exemple pose encore un grave problème qui apparaît lorsque l'expression à fournir pour le vecteur n'est pas une simple variable, mais une expression avec des effets de bord. Voici un exemple :
* (defparameter *elem* 10)
*ELEM*
* (dovec *v* (make-array '(4) :initial-element (decf *elem*))
(print *v*))
8
6
4
2
NIL
*
Ici, les éléments du vecteur devraient tous être 9. Pourtant, une valeur différente est affichée pour chaque élément. Le problème est évident quand on regarde la macro-expansion :
(do* ((#:g1051 0 (1+ #:g1051)))
((>= #:g1051
(length (make-array '(4)
:initial-element (decf *elem*)))))
(let ((*v* (aref (make-array '(4)
:initial-element (decf *elem*))
#:g1051)))
(print *v*)))
Ici, on remarque d'abord la variable générée par gensym dont le nom est #:g1051 (mais il n'est pas possible de capturer une variable avec ce nom). Puis, on remarque que l'expression :
(make-array '(4)
:initial-element (decf *elem*))
non seulement figure deux fois dans l'expansion, mais sera évaluée deux fois à chaque itération de la boucle, une fois dans le test et une autre fois dans le let. Chaque fois qu'elle est évaluée, un nouveau vecteur est créé, et à chaque fois, l'élément initial est diminué d'une unité.
Ce phénomène s'appelle multiple évaluation, et comme le phénomène de capture de variable, il est le plus souvent indésirable. L'utilisateur s'attend probablement à ce que l'expression concernant le vecteur soit évaluée une seule fois avant le début de l'exécution de la boucle.
Voici la solution du problème :
(defmacro dovec (elem-var vector &body body)
(let ((var (gensym))
(vec (gensym)))
`(do* ((,var 0 (1+ ,var))
(,vec ,vector))
((>= ,var (length ,vec)))
(let ((,elem-var (aref ,vec ,var)))
,@body))))
Ici, une deuxième variable générée a été introduite. Elle contiendra la valeur de l'expression du vecteur, qui ne sera donc évaluée qu'une seule fois. Dans le test et le let de la boucle, cette nouvelle variable est utilisée à la place de l'expression initiale. Maintenant, l'exemple fonctionne correctement :
* (defparameter *elem* 10)
*ELEM*
* (dovec *v* (make-array '(4) :initial-element (decf *elem*))
(print *v*))
9
9
9
9
NIL
*
Il est instructif de regarder l'expansion de cette expression :
(do* ((#:g1062 0 (1+ #:g1062))
(#:g1063 (make-array '(4) :initial-element (decf *elem*))))
((>= #:g1062 (length #:g1063)))
(let ((*v* (aref #:g1063 #:g1062)))
(print *v*)))
On voit alors immédiatement que l'expression make-array ne sera évaluée qu'une seule fois.