vendredi 28 novembre 2014

6-LES NOMBRES SOUS LISP

>>>>
Liste des symboles rencontrés dans ce chapitre:
float rationalize numerator denominator realpart imagpart complex + - * / exp expt log isqrt random *random-state* make-random-state troncature arrondi floor ceiling truncate round mod rem = /= < <= > >= min max zerop minusp plusp signum abs evenp oddp gcd lcm sin cos tan asin acos atan sinh cosh tanh asinh acosh atanh *read-base* *print-base* 

Les nombres sous lisp.

Avec CLISP les nombres sont auto-évalués.

LES ENTIERS: c'est la taille de la mémoire qui limite la représentation d'un entier.

Deux variables:

most-positive-fixnum
4611686018427387903
most-negative-fixnum
-4611686018427387904
D'autres variables se construisent avec les mots suivants: most ou least, positive ou negative, short-float, single-float, double-float ou long-float.


LES RATIONNELS:

12       >>    12
-12     >>  -12
12.     >>  12
2/3     >>  2/3
-2/3     >>  -2/3
2/-3     >>  erreur!      il fallait écrire une opération: (/ 2 -3) >> -2/3
4/6     >>  2/3
6/3     >>  2

FLOAT:

(float 1/3)
0.33333334 le nombre rationnel est transformé en flottant
(float 1/3 1d0)
0.3333333333333333d0 ici, en double précision

RATIONALIZE:

(rationalize 2.5)
5/2 2.5 est transformé en rationnel
#b1010     >>  10 la valeur binaire est évaluée dans le système décimal (2³+2¹=10)
#o77     >>  63 la valeur octale est évaluée en base 10: 7*8¹+7*8⁰=63
#xEF     >>  239 la valeur hexadécimale est évaluée en base 10: 14*16¹+15*16⁰=239
#20rJ     >>  19 J, en base 20, vaut 19, en base 10.
#20rK     >>  ERREUR le chiffre K n'existe pas en base 20.

NUMERATOR:
DENOMINATOR:

(numerator 2/3)
2 rend le numérateur du nombre rationnel

(denominator 2/3)
3 rend le dénominateur du nombre rationnel

(denominator 7)
1 rend 1 si l'argument est un nombre entier

LES REELS en virgule flottante courts, simples, doubles ou longs: (et en base 10 uniquement!)

1.0     >>  1.0
12.0     >>  12.0
12e0     >>  12.0 e est le marqueur d'exposant par défaut (initialement: simple)
12e2     >>  1200.0 12*10²
12e-2     >>  0.12 12*10⁻²
0.12     >>  0.12
.12     >>  0.12
0.123E20     >>  1.23e19
123d15     >>  1.23d17 double précision (d)
1.23s2     >>  123.0 court
123s15     >>  1.23e17 court devient simple

LES COMPLEXES:

#c(2    1)   >>  #C(2 1)

REALPART:

(realpart #c(2 1))
2 rend la partie réelle du nombre (complexe ou non)

IMAGPART:

(imagpart #c(2 1))
1 rend la partie imaginaire du nombre complexe (0 si non complexe)

#c(2/3 3/4)  >>  #C(2/3 3/4)
#c(2 1.0)    >>  #C(2.0 1.0)
#c(2.0 1.0d0)>>  #C(2.0d0 1.0d0)
#c(1/2 1.0)  >>  #C(0.5 1.0)
#c(3 0)     >>  3 quand la partie imaginaire est nulle, il s'affiche le nombre réel
#c(1/2 0)    >>  1/2
#c(3 0.0)    >>  #C(3.0 0.0)    sauf dans ce cas
#c(-6/3 0)   >>  -2

Mais:

#c((sqrt 2) 1)
Nous obtenons un message d'erreur disant que (sqrt 2) n'est pas de type réel.
En effet (sqrt 2) est un calcul (non effectué) et non pas un nombre.

La solution est de construire le nombre complexe à l'aide de la fonction suivante:

COMPLEX:

(complex (sqrt 2) 0)
#C(1.4142135 0.0)
Cette fois (sqrt 2) a été calculé et le nombre complexe rendu.
Cette fonction admet deux arguments: la partie réelle et la partie imaginaire.

(realpart (complex (sqrt 2) 0))
1.4142135

Remarque: en lisp les réels et les complexes sont disjoints car ils n'ont pas la même représentation en mémoire.

LES OPERATIONS: +, -, *, /, exp, expt

EXP: rend e élevé à la puissance de l'argument (e est la base des logarithmes naturels).

(exp 1)
2.7182817

(exp 3.2)
24.532532 e puissance 3,2

EXPT: rend le premier argument élevé à la puissance du deuxième argument.

(expt 2.5 2)
6.25 2,5 puissance 2 (ici, le résultat est du type float)

(expt 2 0)
1

(expt 0 0)
1

(expt 0 0.0) >> erreur si le 2ème argument n'est pas un entier

(sqrt (expt -1 3)) <> (expt -1 3/2) deux résultats différents: i et -i !!!!!

Mais les deux résultats sont bons. Voir les fonctions racines ou racines-équation pour a=1, b=0, c=1.
(racine-équation 1 0 1)
Il y a deux racines complexes: 
#C(0.0 1.0)
#C(0.0 -1.0)

(+ 1 2 3) >> 6 le résultat est du même type si tous les arguments sont du même type
(+ 10.0 3.0) >> 13.0
(- 10 3 5) >> 2
(* 2 3 5) >> 30
(/ 10 5 2) >> 1
(expt 2 3) >> 8
(* #c(0 1) #c(1 1)) >> #C(-1 1)

sauf:
(+ #c(1 2) #c(3 -2)) >> 4
(/ 2 3)   >> 2/3
(/ 4) >> 1/4

pour les cas où les arguments sont différents:
(+ 1 2.0) >> 3.0
(/ 2 3.0) >> 0.6666667
(+ #c(1 2) 3) >> #c(4 2)
(+ #c(1 2) 3/2) >> #c(5/2 2)
(expt 2.3 3) >> 12.166999 2,3 et le résultat sont du type float
(expt 5/2 2) >> 25/4 5/2 et le résultat sont du type rational
(expt #c(0 1) 2) >> -1 résultat du type rationnel ou entier
(typep (expt #c(1.2 1) 2) 'complex) >> T le résultat est bien du type complex

Remarques: +,-,/,* sont, en fait, des noms de fonctions.
Avec un seul argument - rend l'opposé de l'argument et / rend l'inverse de l'argument:
(/ 3)        >>        1/3
Avec zéro argument + rend 0 et * rend 1 (c'est à dire les éléments neutres de ces opérations)
   - et / n'admettent pas zéro argument et rendent une erreur.

LOG:

(log 100 10) >> 2.0 logarithme de 100 en base 10

(log 100) >> 4.6051702 logarithme népérien de 100

(log (exp 1))
0.99999994

ISQRT: l'argument doit être un entier positif. Rend le plus grand entier inférieur ou égal à la racine carrée

(isqrt 9) >> 3

(isqrt 3) >> 1

RANDOM: (permet des tirages pseudo-aléatoire)

*RANDOM-STATE*: variable dont la valeur est de type random state (état aléatoire)
la valeur initiale dépend de l'implémentation
c'est l'état aléatoire courant utilisé par la fonction RANDOM si l'argument optionnel
d'état aléatoire n'est pas précisé
MAKE-RANDOM-STATE:     permet de créer un nouvel état aléatoire qui peut être fourni comme valeur
       à *RANDOM-STATE*
Remarque: chaque fois qu'on redémarre SLIME on se trouve dans le même état aléatoire.
  En conséquence, toute application utilisant RANDOM au démarrage risque de sortir les mêmes nombres aléatoires.
  Pour éviter cet inconvénient, on peut changer l'état aléatoire au démarrage de la façon suivante:

(setq *random-state* (make-random-state))

(random 2) >> 0 ou 1 rend 0 ou 1 de façon aléatoire

(random 6)
5 rend un nombre entier entre 0 compris et 6 non compris

(random 2.0) >> ?     rend un nombre flottant entre 0 compris et 2 non compris

On peut se demander si les résultats sont bien équiprobables.
Voici une fonction qui permet de faire un nombre nb de tirages sur des nombres entiers inférieurs à b.
Comme le nombre nb doit être assez grand, on pourra utiliser la fonction factorielle pour calculer nb.
Rappelons, d'abord, les formules de la variance et de l'écart-type:

moy= somme (ni*xi)/N                        V= somme (ni*(xi-moy)²)/N

où N est l'effectif total, les xi représentent la série statistique et les ni les effectifs respectifs de cette série. moy est la moyenne de la série. * représente la multiplication.

(defun tirage-stat (nb b)
           "Rend un aperçu de l'équiprobabilité de b nombres entiers positifs pour un nombre de tirages nb."
           (let ((*h* (make-hash-table :test #'equal)) (*som* 0) (*moy* 0) (*varxnb* 0) (*sigma* 0))
            (clrhash *h*)
   (do ((i 0 (1+ i)))
       ((>= i nb) 'fin)
     (let ((x (random b)))
       (if (not (gethash x *h*))
   (setf (gethash x *h*) 1)
   (setf (gethash x *h*) (1+ (gethash x *h*))))))
   (format t "Série => effectif: ~%")
   (maphash #'(lambda (clé val) (format t "~a => ~a~%" clé val)) *h*)
   (maphash #'(lambda (clé val) (setq *som* (+ *som* (* clé val)))) *h*)
   (setq *moy* (/ *som* nb))      ;somme de la distribution divisé par l'effectif total
   (format t "La moyenne de cette série est: ~,2f.~%" *moy*)
   (maphash #'(lambda (clé val) (setq *varxnb* (+ *varxnb* (* val (expt (- clé *moy*) 2))))) *h*)
   (setq *sigma* (sqrt (/ *varxnb* nb)))
   (format t "Et l'écart-type est: ~,2f.~%" *sigma*)))

Pour la factorielle on pourra prendre la fonction suivante:

(defun fact (n)
   (assert (and (integerp n) (> n -1)))
   (if (zerop n)
       1
       (* n (fact (1- n)))))

Exemple d'appel:

(tirage-stat (fact 10) 6)
Série => effectif: 
2 => 605011
5 => 605498
4 => 604993
3 => 605034
1 => 604360
0 => 603904
La moyenne de cette série est: 2.50.
Et l'écart-type est: 1.71.
NIL
(fact 10) c'est 3628800 tirages!
Les effectifs de la série (0 1 2 3 4 5) montrent que les tirages semblent équiprobables.
l'écart-type est assez important (par rapport à la moyenne), ce qui est une bonne chose.
Je vous laisse réfléchir sur ce résultat et l'interpréter.

Autre point de vue: étudier la série des effectifs obtenus.
Pour cela, il faut modifier un peu le programme tirage-stat:
Dans les calculs la clé est remplacée par la valeur val.

(defun tirage-stat2 (nb b)
           "Rend un aperçu de l'équiprobabilité de b nombres entiers positifs pour un nombre de tirages nb."
           (let ((*h* (make-hash-table :test #'equal)) (*som* 0) (*moy* 0) (*varxnb* 0) (*sigma* 0))
             (clrhash *h*)
   (do ((i 0 (1+ i)))
       ((>= i nb) 'fin)
     (let ((x (random b)))
       (if (not (gethash x *h*))
   (setf (gethash x *h*) 1)
   (setf (gethash x *h*) (1+ (gethash x *h*))))))
   (format t "Série => effectif: ~%")
   (maphash #'(lambda (clé val) (format t "~a => ~a~%" clé val)) *h*)
   (maphash #'(lambda (clé val) (setq *som* (+ *som* (* val val))) clé) *h*)
   (setq *moy* (/ *som* nb))      ;somme de la distribution divisé par l'effectif total
   (format t "La moyenne de cette série est: ~,2f.~%" *moy*)
   (maphash #'(lambda (clé val) (setq *varxnb* (+ *varxnb* (* val (expt (- val *moy*) 2)))) clé) *h*)
   (setq *sigma* (sqrt (/ *varxnb* nb)))
   (format t "Et l'écart-type est: ~,2f.~%" *sigma*)))

(tirage-stat2 (fact 10) 6)
Série => effectif: 
0 => 604043
2 => 604948
1 => 604396
3 => 605157
5 => 604833
4 => 605423
La moyenne de cette série est: 604800.40.
Et l'écart-type est: 460.88.
NIL
         Cette fois l'écart-type est faible par rapport à la moyenne.
Les effectifs sont donc bien concentrés autour de la valeur moyenne.
La fonction random donne donc des résultats assez équiprobables.

TRONCATURE ARRONDI:
FLOOR:
CEILING:
TRUNCATE:
ROUND:

(floor -2.7) >> -3 valeur entière juste inférieure
        0.29999995 distance à la valeur initiale
(floor 2.7) >> 2
        0.70000005
(ceiling -2.7) >> -2 valeur entière juste supérieure
-0.70000005 opposé à la distance à la valeur initiale
(ceiling 2.7) >> 3
-0.29999995
(truncate -2.7) >> -2 comme ceiling pour les nombres négatifs
  -0.70000005
(truncate 2.7) >> 2 comme floor pour les nombres positifs
  0.70000005
(round -2.7) >> -3 arrondi à l'entier le plus proche
        0.29999995
(round 2.7) >> 3
        -0.29999995

Ces 4 fonctions admettent un 2ème argument optionnel: le diviseur.

(floor 5 2)
2 quotient entier de la division
1 reste de la division, le 2ème résultat est donc différent de:

(floor (/ 5 2))
2
1/2             sous cette forme, on obtient la fraction restante

ffloor, fceiling, ftruncate et fround font la même chose, mais rendent un nombre flottant pour le 1er résultat.

DEUX FONCTIONS APPARENTEES: (n'admettent pas plus que deux arguments)

MOD:         (+ (* (floor (/ x y)) y) (mod x y)) === x
REM:         (+ (* (truncate (/ x y)) y) (rem x y)) === x

(mod 10 3)       >>     1         reste de la division entière de 11 par 3. Les restes de
                                                      la division par 3 ne peuvent être que 0, 1, ou 2
(rem 10 3) >>         1              même chose que MOD pour des quotients positifs

(mod -10 3) >> 2                  des résultats différents pour les quotients négatifs
(rem -10 3) >> -1

COMPARAISONS NUMERIQUES:

(= 10 20/2) >> T compare les valeurs quelque soit le type
(eql 10 20/2) >> T car 20/2=10
(= 2 2.0) >> T
mais:
(eql 2 2.0) >> NIL 2 et 2.0 n'ont pas le même type

(/= 1 2 3) >> T                      pris deux à deux les nombres de la liste ne sont pas égaux
(/= 1 2 3 1) >> NIL               deux nombres sont égaux dans la liste

(< 2 3 4) >> T
(< 2 3 3) >> NIL
(<= 2 3 3) >> T

Remarque: en CLISP, =,/=,<,<=,>,>= sont, en fait, des fonctions. On peut, donc, les passer en argument à d'autres fonctions.

MIN:
MAX:

(min 3 7 2 9) >> 2
(min 3 -7 2 9) >> -7
(max 3 -7 2 9) >> 9

ZEROP;
MINUSP:
PLUSP:
pour ces trois prédicats l'argument doit être un nombre

(zerop 5) >> NIL car 5 n'est pas nul
(zerop (- 4 (* 2 2))) >> T l'évaluation de (- 4 (* 2 2)) donne bien zéro
(minusp -7) >> T car -7 est négatif

(minusp -0.0)
NIL

car -0.0 = 0

(plusp -7) >> NIL car -7 n'est pas positif

SIGNUM:

(mapcar #'signum '(-3 5 0 -0.0 0.0))
(-1 1 0 -0.0 0.0)     rend -1 si le nombre est négatif, 1 s'il est positif, 0 s'il est nul (avec une petite variation pour -0.0 et 0.0

En fait,  le résultat donne une indication du type:
(signum -0.5)
-1.0            single-float
(signum -0.5d0)
-1.0d0          double-float

ABS:            (* (abs x) (signum x)) = x

(abs -5.0)
5.0      rend la valeur absolue du nombre

EVENP: (prédicat) l'argument doit être un entier.
rend T si l'argument est pair

(evenp 4) >> T car 4 est pair

ODDP: (prédicat) l'argument doit être un entier.
rend T si l'argument est impair, NIL sinon.

(oddp 5) >> T car 5 est impair

GCD: (gcd &rest integers)
(gcd 60 42)
6
rend le plus grand diviseur commun

LCM: (lcm &rest integers)
(lcm 1 2 3 4 5 6)
60
rend le plus petit multiple commun

FONCTIONS SCIENTIFIQUES:

(exp 1) >> 2.7182817 rend e¹
(log (exp 1)) >> 0.99999994 soit: ln(e)=1
(expt 2 3) >> 8

TRIGONOMETRIE: SIN, COS, TAN, ASIN, ACOS, ATAN.
      L'argument de SIN, COS, TAN doit être en radian ou être un nombre complexe.
      ASIN et ACOS rendent un nombre complexe si l'argument est inférieur à -1, supérieur à 1                    ou est complexe.
      ATAN rend un complexe si l'argument est complexe.

(let ((x (/ pi 3)))
  (list (sin x) (cos x) (tan x)))
(0.8660254037844386d0 0.5000000000000001d0 1.7320508075688767d0)

(* 180 (/ (acos 0.5) pi))
60.000001669652114d0 soit 60 degrés

FONCTIONS HYPERBOLIQUES: SINH, COSH, TANH, ASINH, ACOSH, ATANH.

Voici une petite fonction qui vous permet d'effectuer une liste de calculs:

(defun effectuerl1 (l)
  "effectue chaque action dans la liste, affiche l'action et son résultat."
  (if (listp l)
      (loop for x in l
 do (format t "~a = ~a.~%" x (eval x)))
      (error "L'argument doit être une liste.")))
EFFECTUERL1

(effectuerl1 '((+ 2 3) (- 2 3) (> 2 3) (* 2 3) (/ 2 3) (expt 2 3) (car '(2 3)) (cdr '(2 3)) (cons 2 3)))
(+ 2 3) = 5.
(- 2 3) = -1.
(> 2 3) = NIL.
(* 2 3) = 6.
(/ 2 3) = 2/3.
(EXPT 2 3) = 8.
(CAR '(2 3)) = 2.
(CDR '(2 3)) = (3).
(CONS 2 3) = (2 . 3).
NIL comme on le voit, il n'y a pas que des calculs qui sont effectués.

Changement de base:
Jusqu'à présent, nous avons travaillé en base 10 (système décimal). On le voit à l'aide de la variable *read-base*:

*read-base*
10

Pour changer de base:

(setf *read-base* 8)
8     rend la nouvelle base (en base 10)

15
13 15 en base 8 est auto-évalué à 13 (en base 10)

En base 8, 8 et 9 ne sont pas des nombres et ne sont donc pas auto-évalués:

8
; Evaluation aborted on #.
      on obtient une erreur
    8 et 9 sont alors considérés comme des symboles: |8| et |9|

Donc, quand vous imposez la lecture en base 8, tout nombre formé des chiffres [0 1 2 3 4 5 6 7] est interprété en base 8, puis évalué en base 10.

Les opérations sont possibles:

(+ 10 5)
13 le résultat est exprimé en base 10

(+ 10 1)
9 Le résultat est bien en base 10 car 9 n'existe pas en base 8

(* 10 2)
16 En fait: 8x2 en base 10

Si on fait le calcul en base 8: 10x2=20, et la conversion en base 10 donne: 2x8^1+0x8^0=16.
Comme le résultat est rendu en base 10, parfois on ne voit pas de différence:

(expt 2 3)
8 2 et 3 sont bien interprétés en base 8, le calcul en base 8 donnerait 10.
Mais comme l'affichage est rendu en base 10: 1x8^1+0x8^0=8.

*read-base*
8 la valeur de *read-base* s'affiche aussi en base 10

Revenons à la base 10 (8+2, soit 10+2=12 en base 8)

(setf *read-base* 12)
10      rend la nouvelle base (10)

(* 10 2)
20 résultat correct en base 10

Conclusion: la lecture et l'évaluation se font dans la base contenue dans *read-base* et le résultat est exprimé en base 10.
Mais attention! Voyons ce qui se passe dans un LET:
(ceci suppose que vous êtes bien revenu dans la base 10, sinon 8 serait interprété comme un symbole dans le LET et ceci entraîne une erreur.)

(let ((*read-base* 8)) 9)
9       on s'attend à une erreur car 9 n'existe pas en base 8
  mais 9 n'est pas lu en base 8! Il est évalué en base 10.
  et c'est ce que rend le LET (voir la définition du LET: http://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node83.html)

Essayons:

(let ((*read-base* 8)) (read))
9       la ligne au dessus attend une réponse: on entre 9 et...
|9|   le symbole |9| est rendu (9 n'existe pas en base 8, c'est donc un symbole)

(let ((*read-base* 8)) (read))
14     on entre 14 et ...
12   on obtient 8+4. Cette fois il y a eu lecture et évaluation en base 8, puis affichage en base                   10

On peut vouloir afficher un résultat dans une base spécifiée:

(setf *print-base* 2)
10       10 en base 2, c'est: 1x2^1+0x2^0=2 en base 10
   D'ailleurs, quelque soit la base choisie, on obtient 10 (essayez avec 8)

5
101 5 est lu et évalué en base 10, puis affiché en base 2

(+ 4 5)
1001 le résultat de 4+5 est rendu en base 2

Pour revenir à l'affichage en base 10:

(setf *print-base* 10)
10       la base d'affichage revient à 10

(+ 4 5)
9 C'est bien le résultat en base 10

Compliquons un peu: on veut lire en base 2 et afficher en base 2!

(setf *read-base* 2)
2      pour lire en base 2

(setf *print-base* 10)
10        pour afficher en base 2
  car 10 dans (setf *print-base* 10) est lu en base 2!

(+ 100 101)
1001 4+5 ça fait 9.

(setf *read-base* 1010)
1010    pour revenir à la lecture en base 10 (1010 en base 2)

(setf *print-base* 10)
10       pour revenir à l'affichage en base 10

(+ 4 5)
9 C'est bon!


La prochaine fois: "OPERATIONS LOGIQUES sur les nombres entiers."

dimanche 16 novembre 2014

5-LES MACROS


    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.


Soit à construire une macro do-prem  donnant les nombres premiers entre une valeur minimum et une valeur maximum.
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. 
    (Un 4ème argument est possible: une chaine de caractères qui a la valeur d'une documentation non évaluée)
Exemple:

(define-modify-macro incf1 (&optional (y 1)) +)

(defparameter l nil)
L

(incf1 (car (push 1 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.

(let ((l '(1 2 3)))
  (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)))

(defmacro affich-test (&rest formes)
  "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.