D'abord, j'ai voulu, ici, honorer le travail de Peter Seibel sur son ouvrage "Practical Common Lisp",
en particulier les chapitres "Macros: Defining Your Own" et "Practical: Building a Unit Test Framework".
Ensuite, il s'agissait, pour moi, de le rendre plus accessible aux francophones ayant quelques
difficultés avec la langue anglaise et de le rendre plus intuitif en première lecture.
Mais rien ne vaut de lire et relire Peter Seibel pour son travail pédagogique!
>>>>
Liste des symboles rencontrés dans ce chapitre:
defmacro backquote virgule @ tant-que macroexpand-1 gensym prem répète define-modify-macro
remarques: backquote, virgule et @ ne sont pas des symboles de fonction.
tant-que, prem et répète sont des macro construites.
Vous trouverez, à la suite, d'autres macros construites.
A vous de les améliorer!
Les macros.
MACRO:
DEFMACRO:
Construction de macros:
D'abord il faut dire qu'une macro se définit avec (DEFMACRO nom_macro ...).
On ne peut rien faire avec si peu, mais il s'agit de simplifier l'approche.
Une fois définie, l'appel à la macro se fait par (nom_macro ...).
L'évaluation se fait, alors, en deux temps:
L'expansion: la macro fabrique un code.
L'évaluation du code précédent.
Le résultat est, ensuite, rendu (selon la programmation codée dans la macro.).
Prenons un exemple très simple:
On veut construire une macro pour initialiser une variable à zéro.
Oui... Je sais, DEFVAR ferait l'affaire et mieux!
Mais mon but est de trouver une macro simple.
(defmacro null! (x) ;le paramètre x contiendra la variable (définie ou non)
(list 'setf x 0)) ;à l'expansion cette ligne donne: (setf x 0)
qui est ensuite évaluée.
backquote:
virgule:
Si, à l'appel: (null! var1), var1 n'est pas définie, il y aura un message d'erreur,
mais var1 sera initialisée à zéro... Et, c'est vraiment moins bien que (defvar ...)
N'utilisez pas VAR comme nom de variable, car VAR est dans le package "SB-DEBUG".
Dans les macros plus compliquées, la ligne à expanser peut devenir confuse avec
beaucoup de LIST et de APPEND. Heureusement, il y a le backquote et la virgule!
Le backquote (`) joue le rôle du quote et signale la zone à expanser.
Il devient alors plus facile d'écrire `(setf x 0) et cela a l'avantage d'être compréhensif.
Sauf que x n'a pas à subir l'expansion, et doit être évalué, et c'est la virgule qui va l'indiquer:
`(setf ,x 0)
Notre macro devient alors:
(defmacro null! (x)
`(setf ,x 0))
Là, c'est franchement lisible!
En d'autres termes, quand on fait appel à la macro NULL!, par exemple: (null! a),
le backquote indique la zone à expanser: (setf ,x 0),
,x indique que x doit être évalué: x prend pour valeur a,
l'expansion (la première évaluation) donne: (setq a 0),
la deuxième évaluation est celle de (setq a 0) qui permet à a de prendre la valeur 0.
Alors, compliquons un peu notre macro.
On veut, maintenant, choisir la valeur initiale de la variable. Facile:
(defmacro null! (x init)
`(setf ,x ,init))
Oui, mais null! c'est...nul! Je vous laisse choisir un autre nom.
TANT-QUE: (cette macro est, ici, construite.)
Vous avez compris que ce que nous venons de faire ne sert à rien.
Mais, peut-être, avez vous compris la structure de la définition d'une macro,
sa double évaluation et l'utilisation du backquote (`) et de la virgule (,).
Alors, lançons-nous dans la construction d'une macro plus intéressante.
Vous connaissez la clause WHILE de la macro LOOP.
Nous allons construire une macro TANT-QUE qui lui ressemble.
Par exemple: tant que x <= 10 afficher x (ce qui suppose une incrémentation de x).
On peut le faire avec DO:
(do ((x 0 (1+ x))) ((> x 10)) (format t "~a; " x))
On va utiliser cette macro DO pour construire TANT-QUE.
On constate que le test du DO est le complémentaire du test de la macro TANT-QUE.
Si j'appelle test le test de la macro TANT-QUE, celui du DO est alors: (not test).
La variable x sera introduite dans le test de la macro TANT-QUE.
Cette variable sera déclarée au préalable par (defvar x 0).
Inutile, donc, de déclarer x dans le DO, et à condition de prévoir l'incrémentation de x.
Cela poserait, d'ailleurs, un problème de capture de variable: ce que nous verrons plus loin.
L'action d'affichage sera remplacée par une variable body.
Notre DO devient:
(do () ((not test)) body)
qui est l'expansion de la macro TANT-QUE.
En ajoutant le backquote et les virgules, voici donc la définition de notre macro:
(defmacro tant-que (test body)
`(do () ((not ,test)) ,body))
Cette définition sera bien acceptée, mais:
D'une part, l'appel par:
(tant-que (<= x 10) (format t "~a: " x)) sera catastrophique car x n'est pas incrémenté
et reste, donc, à zéro. La condition de sortie n'est jamais remplie et l'affichage se poursuit
à l'infini.
D'autre part, il n'y a qu'une action possible: (format...).
On peut facilement améliorer cela:
(defmacro tant-que (test &rest body)
`(do () ((not ,test)) ,@body))
Comme vous le savez, &rest permet d'introduire autant d'arguments que l'on veut.
Justement! on voulait, aussi, incrémenter x pour s'assurer une sortie.
Mais, il y a un mais: body n'est plus une simple variable et ,body ne suffit pas.
Notez le @ ajouté entre la virgule et body.
Il permet de transformer une liste en une suite d'éléments, mais seulement dans une forme backquotée.
Pour l'appel à la macro, assurez-vous que la variable x est définie:
(defvar x 0)
et n'oubliez pas d'incrémenter x:
(tant-que (<= x 10) (format t "~a; " x) (incf x))
0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10;
NIL
Ouf!
D'accord ça marche et c'est notre première macro véritable.
Mais, elle est très discutable. Si on oublie l'action (incf x), c'est la catastrophe.
Et, comme pour dire qu'on est nul, plein de zéros vont s'afficher.
Sous EMACS, essayez C-c C-c ou bien C-c C-b pour provoquer l'interruption de SLIME.
Sinon, arrêtez EMACS, sachant que cela peut provoquer la perte de ce qui a été écrit
dans les tampons s'il n'y a pas eu de sauvegarde.
On définit la fonction premier-suivant qui rend le nombre premier qui suit la valeur passée en argument:
(defun premier-suivant (nb)
"rend le nombre premier qui suit le nombre entier positif nb"
(if (and (integerp nb) (>= nb 0)) ;pour tester si nb est bien un entier naturel
(labels ((premier?? (nb) ;définition d'une fonction locale (prédicat) qui vérifie qu'un nombre est premier
(when (> nb 1) ;le nombre testé est supérieur à 1
(loop for k from 2 to (isqrt nb) never (zerop (mod nb k))))))
;rend T si nb n'est jamais divisible par k (2<k<racine de nb)
(loop for n from (1+ nb) when (premier?? n) return n))
;rend le premier nombre premier au dessus de nb
(error "~a n'est pas un nombre entier naturel" nb))) ;message d'erreur sur nb
remarque: (isqrt nb) rend le plus grand nombre entier inférieur à la racine de nb.
On définit l'expression qui va afficher la suite des nombres premiers entre 10 et 20 par exemple:
(do ((p (premier-suivant 10) (premier-suivant p))) ;ces deux premières lignes sont l'expansion
((> p 20)) ;qu'aura à effectuer la macro
(format t "~d " p)) ;cette ligne est le corps passé en paramètre à
la macro (body): voir plus loin.
Le squelette de la macro est de la forme:
(defmacro nom (paramètre)
"documentation optionnelle."
expression-du-corps)
Ce qui va donner:
(defmacro do-prem ((var min max) &body body) ;&body équivaut à &rest
(let ((nom-valeur-fin (gensym))) ;nom de variable générée par (gensym)
`(do ((,var (premier-suivant ,min) (premier-suivant ,var)) ;noter le quoteback et les
(,nom-valeur-fin ,max)) ;virgules devant les variables
((> ,var ,nom-valeur-fin)) ;var est comparée à la variable générée
,@body))) ;noter la virgule et l'arobase
A la 4ème ligne, la variable générée prend la valeur de max et la conserve puisqu'il n'y a pas de saut prévue pour cette variable (max pourrait varier pendant l'itération, suivant l'appel à la macro)
Appel à la macro:
(do-prem (p 0 10) (format t "~d " p))
2 3 5 7 les nombres premiers entre 0 et 10
Lors de l'appel à une macro, celle-ci subit 2 évaluations:
la 1ère donne l'expansion de la macro (qu'on ne voit pas),
et la 2ème donne le résultat final affiché.
MACROEXPAND-1: rend la 1ère évaluation d'une macro.
(macroexpand-1 '(do-prem (p 0 19) (format t "~d " p))) ;pour voir l'expansion de la macro
(DO ((P (PREMIER-SUIVANT 0) (PREMIER-SUIVANT P))
(#:G1345 19)) on voit, ici, le nom de la variable générée
((> P #:G1345))
(FORMAT T "~d " P))
T
Demandons l'évaluation de cette expansion:
(eval (macroexpand-1 '(do-prem (p 0 19) (format t "~d " p))))
2 3 5 7 11 13 17 19 on obtient bien le résultat de la 2ème évaluation de la macro
NIL
GENSYM:
Mais, pourquoi une variable générée?
Si on remplace 19 par (random 100), l'expression (> P #:G1345) deviendrait (> P (random 100)) et random, relancé, prendrait une nouvelle valeur, et non la valeur max du début.
C'est ce qu'on appelle le problème de multiple évaluation dans les macros.
PREM: (macro construite)
Voici une macro toute simple, qui rappelle une procédure du langage LOGO rencontré au collège.
Ici, pas de variable body, et un corps assez court:
(defmacro prem (x)
(list 'car x))
PREM définit une macro prem prenant en argument une liste
On aurait pu la définir, aussi, de cette façon:
(defmacro prem (x)
`(car ,x))
PREM le "backquote" joue presque le même rôle que "quote", sauf qu'on
peut "déquoter" à l'intérieur de la forme "backquotée". C'est la virgule qui permet de le faire.
On la place avant les variables pour qu'elles ne soient pas évaluées à la première évaluation
de la macro. Les variables, en générale, sont évaluées à la deuxième évaluation.
Autre intérêt du "backquote": l'écriture devient plus simple et compréhensible (imaginez
tous les "list" et "append" qu'il faudrait mettre dans des macros plus complexes!).
(prem '(a b c d e))
A prem rend le premier élément de la liste
(macroexpand-1 '(prem '(a b c d e)))
(CAR '(A B C D E)) macroexpand-1 rend la 1ère évaluation de la macro, c'est à dire l'expansion.
T
Remarque:
(list 'car '(a b c d e)) ou: (list 'car x), si x est définie, donne:
(CAR (A B C D E)) on remarque que quote (') manque devant la liste: ce n'est pas l'expansion précédente: la variable x n'est évaluée qu'après l'expansion dans une macro.
On peut utiliser un backquote (`) dans la définition de la macro pour l'expression à expanser:
(defmacro prem (x)
`(car ,x))
la virgule indique que x ne subit pas l'expansion.
Remarque: comme car est setf-able, prem aussi.
(defparameter l '(a b c d))
L
(setf (prem l) 9)
9
l
(9 B C D)
Utilisation du backquote dans les expressions de macro à expanser (exemples):
`(a (+ 1 2) c) >> (a (+ 1 2) c)
`(a ,(+ 1 2) c) >> (a 3 c)
`(a (list 1 2) c) >> (a (list 1 2) c)
`(a ,(list 1 2) c) >> (a (1 2) c)
`(a ,@(list 1 2) c) >> (a 1 2 c)
ou encore:
(defvar a 10) >> A
(defvar b '(1 2 3)) >> B
`(a vaut ,a) >> (A VAUT 10)
`(b vaut ,b) >> (B VAUT (1 2 3))
`(les éléments de b sont ,@b) >> (LES ÉLÉMENTS DE B SONT 1 2 3)
Donc avec le backquote (`) aucun élément de la liste n'est évalué (comme avec quote: '), sauf les éléments précédés d'une virgule.
,@ insère les éléments de la liste dans la liste résultante.
RÉPÈTE: (macro construite)
Un autre exemple: la macro répète.
(defmacro répète (n &rest body)
`(do ((i 0 (+ i 1)))
((>= i ,n))
,@body))
(let ((x 10))
(répète 5 (incf x))
x)
15 la macro semble bien fonctionner: x a bien été incrémentée 5 fois.
Oui, mais:
(let ((i 10))
(répète 5 (incf i))
i)
10 on a juste changé le nom de la variable: et ça ne marche plus!
On dit dit que la macro a effectué une capture de variable.
En effet: c'est la variable i qui intervient dans la définition de la macro répète.
Pour éviter cela, on fait intervenir une variable générée dans la définition de la macro:
(defmacro répète (n &rest body)
(let ((g (gensym)))
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))
Aucune autre variable ne pourra être capturée par la variable générée.
Cependant, il reste le problème de la multiple évaluation:
Si n est remplacée par une fonction, cette fonction sera évaluée dans chaque itération du DO.
Comme on l'a vu plus haut, on fait intervenir encore (gensym):
(defmacro répète (n &rest body)
(let ((g (gensym))
(h (gensym)))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))
Retenez-donc qu'avec les macros il y a souvent ces deux problèmes à surveiller:
La multiple évaluation et la capture de variable.
Il y a un autre problème... C'est quand l'expansion de la macro est un SETF.
Prenons un exemple avec la fonction d'incrémentation INCF:
(defparameter l nil)
L pour définir une liste vide
(incf (car (push 1 l)))
2 rend le 1er élément (1) de l incrémenté de 1
l
(2) en effet l contient bien 2
Ici, on redéfinit la macro INCF:
On l'appelle INCF1, et il semble évident de la définir comme suit:
(defmacro incf1 (x &optional (y 1))
`(setf ,x (+ ,x ,y)))
Testons cette macro comme précédemment avec INCF:
(defparameter l nil)
L pour définir une liste vide
(incf1 (car (push 1 l)))
2 il semble que cela a fonctionné, mais:
l
(1 2) l ne contient pas ce que l'on espérait: (1 2) au lieu de (2)
Regardons l'expansion de INCF1:
(macroexpand-1 '(incf1 (car (push 1 l))))
(SETF (CAR (PUSH 1 L)) (+ (CAR (PUSH 1 L)) 1))
T
Interprétons: le 1er (push 1 l) transforme l en (1) et (car (push 1 l)) rend 1 (l'index pour le SETF).
Rappelons que cela est possible grace à la fonction (setf car) qui existe bien (essayez: #'(setf car)).
Le 2ème (push 1 l) transforme l en (1 1), puis (+ (car (push 1 l)) 1) calcule la valeur à placer à l'index 1: c'est à dire 2.
Quand une macro fait appel à SETF, l'expansion de la macro ne rendra pas l'effet désiré.
Heureusement:
DEFINE-MODIFY-MACRO: permet de définir des classes restreintes de macros construites sur SETF.
3 arguments: le nom de la macro, ses paramètres additionnels (la place est implicitement le premier paramètre) et le nom de la fonction calculant la valeur de la place précédemment désignée.
Exemple:
(define-modify-macro incf1 (&optional (y 1)) +)
(defparameter l nil)
L
2
l
(2) cette fois INCF1 fonctionne comme INCF
l'expansion est:
(macroexpand-1 '(incf1 (car (push 1 l))))
(LET* ((#:G1502 (PUSH 1 L)) (#:NEW1499 (+ (CAR #:G1502) 1)))
(SB-KERNEL:%RPLACA #:G1502 #:NEW1499))
T on voit apparaître, ici, deux variables intermédiaires: #:G1502 (une liste) et #:NEW1499 (une valeur). la fonction RPLACA remplace le 1er élément de la liste (CAR) par la valeur.
Définissons, maintenant une nouvelle macro MET-DER qui place un élément à la fin d'une liste:
(define-modify-macro met-der (élém)
(lambda (liste élém) (append liste (list élém))) "Cette macro ajoute un élément à la fin de la liste")
La définition comporte, cette fois, une documentation.
(met-der l 4)
l)
(1 2 3 4) l'élément 4 est ajouté à la fin de la liste (1 2 3)
Remarque: notez bien que l'appel à met-der se fait avec deux arguments et non pas un seul comme il apparaît dans la définition de la macro avec define-modify-macro.
Voyez, aussi, la différence entre l'appel à cette macro et l'appel à une simple fonction lambda:
(defparameter liste '(1 2 3))
LISTE
(defparameter élém 4)
ÉLÉM
Appel à la fonction lambda:
(funcall #'(lambda (liste élém) (append liste (list élém))) liste élém)
(1 2 3 4)
liste
(1 2 3) la liste n'est pas modifiée
Appel à la macro met-der:
(met-der liste élém)
(1 2 3 4)
liste
(1 2 3 4) la liste est modifiée
Quelques macros utiles.
Il existe une clause FOR dans LOOP, mais il n'y a pas de fonction FOR dans le langage LISP.
Voici une macro FOR qui fera l'affaire:
(defmacro for (var début fin &body body)
"effectue une itération sur var variant de début à fin et rend à chaque itération le résultat de body."
(let ((gfin (gensym)))
`(do ((,var ,début (1+ ,var))
(,gfin ,fin))
((> ,var ,gfin))
,@body)))
(for x 5 9
(princ x))
56789
NIL
La macro IN suivante est un prédicat qui vérifie si un élément est présent dans une liste:
(defmacro in (élément &rest suite)
"rend T si élément est dans la suite, NIL sinon."
(let ((gin (gensym)))
`(let ((,gin ,élément))
(or ,@(mapcar #'(lambda (c) `(eql ,gin ,c))
suite)))))
Remarquez la présence de deux backquotes dans cette macro IN.
MAPCAR (voir plus loin MAPCAR:) rend une liste, mais comme il y a ,@ devant, cette liste est transformée en suite
et OR s'applique parfaitement sur cette suite.
(in #'+ #'* #'+ #'-)
T l'opération + est bien dans le groupe d'opérations qui suit
(in #'+ #'* #'-)
NIL l'opération + n'est pas parmi les opération suivantes
Voici une macro qui choisit, de façon aléatoire, une action dans une suite d'évênements possibles,
et rend son résultat:
(defmacro action-aléatoire (&rest suite)
"effectue de façon aléatoire une des actions de la suite et rend son résultat."
`(case (random ,(length suite))
,@(let ((index -1))
(mapcar #'(lambda (x)
`(,(incf index) ,x))
suite))))
Là aussi, on remarque les deux backquotes.
MAPCAR (voir plus loin MAPCAR:) et donc LET rendent une liste transformée en suite avec ,@
ce qui convient à la construction du CASE (voir plus haut CASE:).
(action-aléatoire 'a 'b 'c 'd 'e)
B l'élément B a été choisi de façon aléatoire
(action-aléatoire (+ 3 4) (* 3 4) (- 3 4) (/ 3 4))
12 ici, c'est l'opération (* 3 4) qui a été choisie
Macro calculant une moyenne:
(defmacro moyenne (&rest arguments)
"rend la moyenne des arguments."
`(/ (+ ,@arguments) ,(length arguments)))
(moyenne 1 2 3 4)
5/2
Une macro permettant l'accès à plusieurs variables générées par gensym:
(defmacro multi-gensyms (liste &body body)
`(let ,(mapcar #'(lambda (s)
`(,s (gensym)))
liste)
,@body))
Par exemple, pour définir une macro calculant la moyenne de trois valeurs sans risque d'évaluations multiples:
(defmacro moy-tirages (x y z)
(multi-gensyms (a b c) ;a, b, c prennent pour valeurs des variables générées
(format t "~a ~a ~a~%" a b c) ;pour montrer les variables générées
`(let ((,a ,x) (,b ,y) (,c ,z)) ;les variables générées prennent les valeurs de x, y et z
(format t "~a ~a ~a~%" ,a ,b ,c) ;affiche les valeurs de x, y et z sans risque d'évaluations multiples
(moyenne ,a ,b ,c)))) ;calcule la moyenne des trois valeurs avec la macro précédente
(defun dé ()
(random 6))
définit un tirage au sort avec un dé
(moy-tirages (dé) (dé) (dé))
G1628 G1629 G1630 3 variables générées
4 1 1 les 3 valeurs du tirage
2 la moyenne
Voici une macro SI avec capture volontaire et possibilité d'utiliser la variable IT dans les résultats:
(defmacro si (calc alors &optional sinon)
`(let ((it ,calc)) ;IT prend la valeur de calc (qui peut être un prédicat)
(if it ,alors ,sinon))) ;si IT est non NIL...ALORS est évalué,sinon...SINON est évalué
(defparameter x 10)
X
(si (+ (* 3 x x) (* 5 x)) (format t "Le résultat est: ~a.~%" it) (format t "Pas de résultat."))
Le résultat est: 350.
NIL
(si (> 3 x) (format t "Le résultat est: ~a.~%" it) (format t "Pas de résultat."))
Pas de résultat.
NIL
Pourquoi pas un IF qui travaille à la mode progn?
A améliorer peut-être:
(defmacro ifprogn (test (&rest alors) (&rest sinon))
(let ((it (gensym)))
`(let ((,it ,test))
(cond
(,it ,@alors)
(t ,@sinon)))))
Attention à l'appel:
(ifprogn test ((...) (...) ...) ((...) (...) ...)) après l'action test, il y a deux listes d'actions (listes de listes)
Exemple:
(defun essai ()
(répète 20 (ifprogn (< x 10)
((setf x (+ x 2.0)) (format t "augmentation: x = ~a.~%" x))
((setf x (/ x 3)) (format t "diminution: x = ~a.~%" x)))))
(setf x 0)
0
(essai)
augmentation: x = 2.0.
augmentation: x = 4.0.
augmentation: x = 6.0.
augmentation: x = 8.0.
augmentation: x = 10.0.
diminution: x = 3.3333333.
augmentation: x = 5.333333.
augmentation: x = 7.333333.
augmentation: x = 9.333333.
augmentation: x = 11.333333.
diminution: x = 3.7777777.
augmentation: x = 5.7777777.
augmentation: x = 7.7777777.
augmentation: x = 9.777778.
augmentation: x = 11.777778.
diminution: x = 3.925926.
augmentation: x = 5.925926.
augmentation: x = 7.925926.
augmentation: x = 9.925926.
augmentation: x = 11.925926.
NIL on voit que les différents résultats se rapprochent de 4, 6, 8, 10, 12
compte-tenu de la précision des nombres (simple ici), on peut s'attendre à des résultats cycliques:
répéter 4 fois (essai) pour le voir
Macro calculant la nième expression:
(defmacro la-nième (n &rest expression)
(let ((p (gensym)))
`(let ((,p (1- ,n)))
(if (>= ,p 0)
(nth ,p (list ,@expression))))))
(la-nième 2 (+ 1 2) (+ 2 3) (+ 3 4) (* 3 4))
5 rend le résultat de (+ 2 3)
Macro qui double la valeur d'une variable:
(defmacro doubler (x)
(let ((x1 (gensym)))
`(let* ((,x1 ,x) (x (* 2 ,x1)))
x)))
Noter le LET* qui permet de calculer X (sans virgule) en fonction de la valeur précédente de ,X1.
(doubler 23)
46
UNITE DE TEST: (les fonctions et macros sont regroupées à la fin pour recopie et compilation dans REPL)
(defvar *nom-test* nil)
*NOM-TEST* définition d'une variable dynamique qui
contiendra le nom du test effectué
Pour bien comprendre la suite, il est utile de s'informer sur les chaînes de contrôle de format, ses directives et modificateurs.
Donc: Ctrl+S format: ou bien voir le chapitre format si vous n'utilisez pas emacs.
définition d'une fonction permettant d'afficher le résultat d'un test (un seul ici):
(affich-test est une macro traitée plus loin.)
(defun affichage-résultat (resultat forme)
"Affiche le résultat d'un seul test passé en argument dans forme. Appelé par affich-test."
(format t "~:[ECHEC~;OK~] ... ~a: ~a~%" resultat *nom-test* forme)
resultat)
ici *nom-test* vaut NIL (voir, plus loin, les catégories de test)
Format affiche ECHEC ou OK suivant que resultat est NIL ou T respectivement, suivi de ... , puis de la valeur de *nom-test* (NIL ici), suivi de : , puis de l'évaluation de forme.
La fonction affiche-résultat rend l'évaluation de résultat, donc NIL ou T (ou tout autre valeur équivalente à T).
Exemple:
(affichage-résultat (= (+ 1 2) 3) '(= (+ 1 2) 3))
OK ... NIL: (= (+ 1 2) 3) cas d'un test menant à une réussite
T T est le résultat de (= (+ 1 2) 3)
(affichage-résultat (= (+ 1 2) 4) '(= (+ 1 2) 3))
ECHEC ... NIL: (= (+ 1 2) 3) cas d'un test menant à un échec
NIL NIL est le résultat de (= (+ 1 2) 4)
(affichage-résultat 4 '(= (+ 1 2) 3))
OK ... NIL: (= (+ 1 2) 3)
4 ici, le résultat vaut 4, considéré comme vrai.
Vous avez bien constaté, sur ces trois derniers exemples, que c'est résultat qui est testé et non forme.
forme est donc erroné dans les deux derniers exemples: forme doit correspondre à résultat mais quoté.
forme est là pour rappeler dans l'affichage ce qui a été testé.
Il aurait fallu écrire, par exemple, dans le deuxième cas:
(affichage-résultat (= (+ 1 2) 4) '(= (+ 1 2) 4))
ECHEC ... NIL: (= (+ 1 2) 4)
NIL
Cette répétition, peu souhaitable, peut être évitée à l'aide d'une macro.
Une macro va permettre de passer plusieurs tests en série: (1ère version)
(defmacro affich-test (&rest formes)
`(progn
,@(loop for f in formes collect `(affichage-résultat ,f ',f))))
On voit que la répétition résultat-forme des arguments de affichage-résultat se transforme en ,f ',f.
La macro, elle, n'utilise pas de répétition.
Exemple:
(affich-test
(= (+ 1 2) 3) ;tous les tests sont écrits à la suite
(= (+ 1 2 3) 7)
(= (+ -1 -3) -4))
OK ... (= (+ 1 2) 3)
ECHEC ... (= (+ 1 2 3) 7) un test est en échec
OK ... (= (+ -1 -3) -4)
T mais ce résultat est un inconvénient
il faudrait: NIL
Une autre macro va permettre de modifier ce résultat:
(defmacro résultat-global (&rest formes)
(let ((resultat (gensym))) ;appel à une variable générée
`(let ((,resultat t)) ;la variable resultat prend la valeur T
,@(loop for f in formes
collect `(unless ,f (setf ,resultat nil))) ;s'il y a un échec, resultat bascule sur NIL
,resultat))) ;le résultat global est affiché
La macro affich-test doit être modifiée: (2ème version)
(defmacro affich-test (&rest formes)
`(résultat-global
,@(loop for f in formes collect `(affichage-résultat ,f ',f))))
Exemple:
(affich-test (= (+ 1 2) 3) (= (+ 1 2 3) 6) (= (+ -1 -3) -4)) ;tous les tests sont écrits à la suite
OK ... (= (+ 1 2) 3)
OK ... (= (+ 1 2 3) 6)
OK ... (= (+ -1 -3) -4)
T résultat global: T
(affich-test (= (+ 1 2) 3) (= (+ 1 2 3) 7) (= (+ -1 -3) -4)) ;tous les tests sont écrits à la suite
OK ... (= (+ 1 2) 3)
ECHEC ... (= (+ 1 2 3) 7) un échec
OK ... (= (+ -1 -3) -4)
NIL résultat global: NIL
________________________________________________________
La fonction affichage-résultat peut être placée en LABELS de la macro affich-test:
(defmacro affich-test (&rest formes)
`(labels ((affichage-résultat (resultat forme)
(format t "~:[ECHEC~;OK~] ... ~a~%" resultat forme)
resultat))
(résultat-global
,@(loop for f in formes collect `(affichage-résultat ,f ',f)))))
________________________________________________________
On peut, ici, proposer des catégories de tests:
Pour des tests sur l'addition
(defun test+ ()
(let ((*nom-test* 'test+))
(affich-test ;on utilise affich-test
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4))))
Pour des tests sur la multiplication
(defun test* ()
(let ((*nom-test* 'test*))
(affich-test ;on utilise affich-test
(= (* 2 2) 4)
(= (* 3 5) 15)
(= (* -2 7) -14))))
Pour regrouper les tests précédents
(defun test-arithmétique ()
(résultat-global ;Attention! on utilise résultat-global
(test+)
(test*)))
Exemple:
(test-arithmétique)
OK ... (TEST-ARITH TEST+): (= (+ 1 2) 3)
OK ... (TEST-ARITH TEST+): (= (+ 1 2 3) 6)
OK ... (TEST-ARITH TEST+): (= (+ -1 -3) -4)
OK ... (TEST-ARITH TEST*): (= (* 2 2) 4)
OK ... (TEST-ARITH TEST*): (= (* 3 7) 21)
OK ... (TEST-ARITH TEST*): (= (* -2 6) -12)
T
ici, tous les tests sont bons
Pour construire des catégories de test, on peut avoir recours à une autre macro:
(defmacro def-catégorie (nom paramètres &rest body)
`(defun ,nom ,paramètres
(let ((*nom-test* ',nom))
,@body)))
Exemple:
(def-catégorie test+ () ;on redéfinit la fonction test+
(affich-test ;pour combiner deux types de test, on utilise résultat-global
(= (+ 1 2) 3) ;au lieu de affich-test
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))
TEST+
(test+)
OK ... TEST+: (= (+ 1 2) 3)
OK ... TEST+: (= (+ 1 2 3) 6)
OK ... TEST+: (= (+ -1 -3) -4)
T
*nom-test* a bien été prise en compte par la macro
Voici toutes les fonctions précédentes regroupées pour utilisation dans REPL:
(defvar *nom-test* nil)
(defun affichage-résultat (resultat forme)
"Affiche le résultat d'un seul test passé en argument dans forme. Appelé par affich-test."
(format t "~:[ECHEC~;OK~] ... ~a: ~a~%" resultat *nom-test* forme)
resultat)
(defmacro résultat-global (&rest formes)
"Evalue, dans l'ordre, tous les tests passés en paramètre dans formes. Rend un résultat globale: T si tout est bon, NIL si un résultat ou plusieurs sont faux"
(let ((resultat (gensym))) ;appel à une variable générée
`(let ((,resultat t))
,@(loop for f in formes collect `(unless ,f (setf ,resultat nil)))
,resultat)))
"Contrôle tous les tests proposés dans formes. fait appel à résultat-global et affichage-résultat."
`(résultat-global
,@(loop for f in formes collect `(affichage-résultat ,f ',f))))
(defmacro def-catégorie (nom paramètres &rest body)
"permet de définir une fonction de test: en faisant appel à affich-test. On peut aussi définir une fonction de test regroupant d'autres fonctions de test en faisant appel à résultat-global."
`(defun ,nom ,paramètres
(let ((*nom-test* (append *nom-test* (list ',nom))))
,@body)))
Appels:
(def-catégorie test* () (affich-test &rest formes))
(def-catégorie test-global () (résultat-global &rest test*))
Notez bien que ces fonctions et macros vous sont fournies comme aide-mémoire.
Donc cela peut vous sembler peu pédagogique!
Je n'avais pas l'intention de reprendre le très bon travail pédagogique de Peter Seibel dans son "Practical Common Lisp".
Voyez donc pour cela le chapitre 9 (En anglais).
Pour les francophones:
J'ai juste francisé les fonctions et les macros pour une meilleure compréhension du code.
Une fois encore: merci à Peter Seibel.
La prochaine fois: Les nombres sous Lisp.
Aucun commentaire:
Enregistrer un commentaire