lundi 26 janvier 2015

15-CLASSE D'OBJET et initialisation

>>>>
Classe d'objet.

CLASS-OF: pour trouver la classe d'un objet.

(class-of 'a)
#&ltBUILT-IN-CLASS SYMBOL&gt
                    'a est un objet de la classe SYMBOL

(class-of "a")
#&ltBUILT-IN-CLASS SB-KERNEL::SIMPLE-CHARACTER-STRING&gt
                      "a" est un objet de la classe SB-KERNEL::SIMPLE-CHARACTER-STRING

(class-of '(a b))
#&ltBUILT-IN-CLASS CONS&gt '(a b) est un objet de la classe CONS

C'est pour dire que des objets vous en connaissez déjà!
Dans la suite, nous utiliserons DEFCLASS pour définir une classe, MAKE-INSTANCE pour définir des instances de cette classe (objets), et DEFMETHOD pour définir des méthodes sur ces objets.
On peut construire des méthodes sur des objets LISP déjà définis et on peut utiliser des fonctions LISP sur des objets créés avec DEFCLASS et MAKE-INSTANCE.

Je ne prétends pas, dans la suite de ce travail, faire tout dans les règles de l'art!
Je vous laisse, en fait, beaucoup de travail.
Il vous faudra améliorer les classes, les fonctions, les méthodes, et contrôler un peu mieux les erreurs.
C'est le but de ce blog: vous faire travailler.

DEFCLASS:
C'est une macro qui permet de définir de nouveaux types de données.

Pour aborder certaines définitions, on se contente (pour l'instant) de définir une classe "point":

(defclass point ()
   ((x :accessor abscisse-point ;pour obtenir l'abscisse du point ou la modifier
       :writer put-abs ;pour modifier l'abscisse
       :reader get-abs ;pour obtenir l'abscisse
       :reader x-de ;un 2ème reader plus pratique
       :initarg :x ;clé utilisée pour initialiser l'instance d'un point (son abscisse), sinon:
       :initform 0 ;valeur par défaut de l'abscisse
       :type number ;l'abscisse est du type number
       :documentation "L'abscisse du point.")     ;pour rappeler qu'on parle, ici, de l'abscisse
    (y :accessor ordonnée-point       ;même remarques pour le slot y que pour le slot x
       :writer put-ord
       :reader get-ord
       :reader y-de
       :initarg :y
       :initform 0
       :type number
       :documentation "L'ordonnée du point.")
    (z :accessor altitude-point ;même remarques pour le slot z que pour le slot x
       :writer put-alt
       :reader get-alt
       :reader z-de
       :initarg :z
       :initform 0
       :type number
       :documentation "L'altitude du point."))
   (:documentation "La classe de l'objet point.")) ;une documentation de la classe point
#<STANDARD-CLASS COMMON-LISP-USER::POINT>

On voit que cette classe "point" n'hérite pas explicitement d'une superclasse car la parenthèse qui suit DEFCLASS POINT est vide.
Cependant toute classe créée hérite de T et de standard-class.
La classe standard-class est la classe par défaut des classes définies par defclass.
La classe standard-object est une instance de standard-class et est une superclasse de chaque classe qui est une instance de standard-class sauf elle-même.

Vous avez constaté que les slots x, y, z possèdent 2 readers. C'est possible (et même plus).
On pourrait aussi ajouter des :accessor et des :initarg.

Pour obtenir une instance d'un point:

(setf A (make-instance 'point :x 2 :y 3 :z -1))
#<POINT {1002A70D43}>

Mais pour mieux contrôler les données, on préfère utiliser un constructeur:

(defun make-point (x y z)
   (make-instance 'point :x x :y y :z z))
MAKE-POINT

(defvar A (make-point 2 3 -1))
A
Effectivement, c'est un peu plus pratique.
Mais le point par défaut, si on ne connait pas ses coordonnées, ne peut pas être obtenu ainsi.

par contre:

(defvar O (make-instance 'point))

O le point O est défini: ses coordonnées sont (0 0 0).
(ce sont les :initform qui fournissent les 3 valeurs 0.

FIND-CLASS:

(find-class 'point)
#<STANDARD-CLASS COMMON-LISP-USER::POINT>
"point" est une classe standard de common-lisp-user.
 
Vérifions le type de A:

(typep A 'point)
T
A est bien du type point.

CLASS-OF:

Pour chercher la classe de A, on fait:

(class-of A)
#<STANDARD-CLASS COMMON-LISP-USER::POINT>

Et la classe de la classe:

(class-of (class-of A))
#<STANDARD-CLASS COMMON-LISP:STANDARD-CLASS>
On dit que standard-class est la métaclasse de A.
                 A est l'instance d'une classe qui est, elle-même, l'instance d'une class CLOS
(Common Lisp Object System).

(typep A (class-of A))
T
heureusement!

Pour en savoir plus sur A:

(describe A)
#<POINT {1002A70D43}>
  [standard-object]

Slots with :INSTANCE allocation:
  X                              = 2.5
  Y                              = 3
  Z                              = -1
; No value

SLOT-VALUE:

(slot-value A 'x)
2
On vérifie, ainsi, que l'abscisse de A est bien 2.
Notez que SLOT-VALUE est setf-able:
On peut donc donner une valeur à l'abscisse de A (où la modifier).

(setf (slot-value A 'x) 2.5)
2.5

WITH-SLOTS:

Pour avoir les 3 coordonnées de A:

(with-slots (x y z)
     A
   (format t "Les coordonnées de ce point sont (~a,~a,~a).~%" x y z))
Les coordonnées de ce point sont (2.5,3,-1).
NIL

Il serait plus intéressant d'avoir une fonction qui rende ces coordonnées.
On définit d'abord une fonction générique (sinon, elle serait de toute façon créée implicitement):

DEFGENERIC:

(defgeneric coord-de (obj)
   (:documentation "Rend les coordonnées de l'objet passé en argument."))
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::COORD-DE (0)>

Notez que l'argument passé à la fonction générique est obj (et non point).
On précise la méthode de cette fonction pour la classe considérée: point.

DEFMETHOD:

(defmethod coord-de ((obj point))
   (with-slots (x y z)
       obj
     (list x y z)))
#<STANDARD-METHOD COMMON-LISP-USER::COORD-DE (POINT) {1004BCB363}>

Cette méthode est valable pour la classe point, mais on pourrait créer une méthode de même nom pour une autre classe.
(par exemple, une sous-classe pour un plan où chaque point n'a que 2 coordonnées
ou encore une classe vecteur, car on peut aussi vouloir calculer les coordonnées d'un vecteur).
Dans la méthode, ci-dessus, il n'y a pas beaucoup de contrôles:
L'objet est-il définit? Les coordonnées existe-t-elles? Toutes?
Je vous laisse travailler... Si vous le voulez bien.
Voyez, à ce sujet, quelques éléments plus bas (slot-boundp, slot-exists-p)

(coord-de A)
(2.5 3 -1)
La méthode rend bien les coordonnées du point A.

SLOT-BOUNDP:

Pour vérifier si un spécificateur est définit sur l'instance A (c'est à dire s'il a une valeur):

(slot-boundp A 'y)
T
l'ordonnée du point A est bien définie.

SLOT-EXISTS-P:

Pour vérifier l'existence d'un spécificateur sur une instance A (même s'il n'a pas de valeur):

(slot-exists-p A 'z)
T
l'altitude z existe bien

(slot-exists-p A 't)
NIL
ce slot n'existe pas

Les propriétés des spécificateurs (options):
On accède à ces propriétés par mots-clés: :reader, :writer, :accessor, :allocation, :initarg, :initform, :type, :documentation.

:accessor nous permet de préciser le nom d'une fonction pour lire ou écrire la valeur d'un spécificateur (à la place de slot-value).
L'accesseur de y (par exemple) est ordonnée-point:

(ordonnée-point A)
3
on obtient bien l'ordonnée du point A.

WITH-ACCESSORS: comme WITH-SLOTS, mais il faut donner une liste de fonctions.

(with-accessors ((x abscisse-point) (y ordonnée-point) (z altitude-point))
     A
   (format t "Les coordonnées de ce point sont (~a,~a,~a).~%" x y z))
Les coordonnées de ce point sont (2.5,3,-1).
NIL

(type-of #'ordonnée-point)
STANDARD-GENERIC-FUNCTION

(setf (ordonnée-point A) 3.5)
      Pour modifier l'ordonnée.

:writer sert à définir une fonction qui écrit la valeur du spécificateur (mais qui ne permet pas de la lire).
La différence avec :accessor est qu'on n'a pas besoin de SETF pour donner une valeur au spécificateur:
On utilise la fonction d'accès suivi de la valeur et de l'objet (et dans cet ordre).
:reader sert à définir une fonction qui lit la valeur du spécificateur (mais qui ne permet pas de l'écrire).

(get-ord A)
3.5
Rend l'ordonnée du point A.

ou bien:

(y-de A)
3.5
En effet, il y a un 2ème reader.

(type-of #'y-de)
STANDARD-GENERIC-FUNCTION
Les 2 fonctions reader sont des fonctions génériques standard

(put-ord 3 A)
3
Change l'ordonnée du point A. En effet:

(get-ord A)
3
L'ordonnée a bien changée.
Dans la définition de la classe "point" vous pouvez, bien sûr, donner des noms différents de get-ord et put-ord.

Un peu comme describe, on peut analyser un objet de cette façon:

(inspect A)

The object is a STANDARD-OBJECT of type POINT.
0. X: 2.5
1. Y: 3
2. Z: -1
> q
à la place de q (pour quitter l'inspection) on peut taper 0 ou 1 ou 2 pour avoir des renseignements sur x ou y ou z.

; No value
l'inspection ne rend pas de valeur.

:initarg définit une clé pour accéder à la valeur du spécificateur.
Les clés :x,  :y, et :z ont l'avantage d'être courte.

:initform définit une valeur par défaut du spécificateur.
Autrement dit, quand vous définissez une instance de point, si vous ne donnez pas l'abscisse du point, celle-ci prendra la valeur par défault ici: 0 (zéro).

:type permet de définir le type du spécificateur (dans notre cas, c'est un nombre).

(type-of (abscisse-point A))
SINGLE-FLOAT
L'abscisse du point A est un SINGLE-FLOAT

:documentation permet de définir une chaîne de caractères tenant lieu de documentation sur le spécificateur:
Par exemple, la documentation sur le spécificateur y est "L'ordonnée du point.".
Cette documentation est donc une propriété du spécificateur.

Par contre si :documentation est placé au même niveau que la liste des spécificateurs, la documentation porte sur la classe.
(ici, documentation sur la classe "point").

(documentation 'point 'type)
"La classe de l'objet point."
       Rend la documentation sur la classe "point".
   Remarquer que le type de cette documentation est 'type;

:allocation précise si le slot est local ou partagé:
:allocation :instance ou par défaut, signifie que le slot peut être différent pour chaque instance.
:allocation :class est une propriété qui permet de préciser que le spécificateur aura la même valeur pour toutes les instances.
Même si on change la valeur pour une instance, toutes les autres prendront cette nouvelle valeur.

_________________________________

Construisons une classe vecteur-lié.
Un vecteur lié est défini par un point origine et un point extrémité.
Nous avons, donc, besoin de deux slots (ou spécificateurs): origine et extrémité.
Ces deux slots sont du type point.

(defclass vecteur-lié (point)
   ((origine :accessor origine-vecteur
     :writer change-origine
     :reader origine-de
     :initarg :origine-vect
     :initform (eval (defparameter O (make-point 0 0 0)))
     :type point
     :documentation "L'origine du vecteur.")
    (extrémité :accessor extrémité-vecteur
       :writer change-extrémité
       :reader extrémité-de
       :initarg :extrémité-vect
       :initform (eval (defparameter I (make-point 1 0 0)))
       :type point
       :documentation "L'extrémité du vecteur."))
   (:documentation "La classe de l'objet vecteur-lié."))
#<STANDARD-CLASS COMMON-LISP-USER::VECTEUR-LIÉ>

Voici le constructeur:

(defun make-vect (orig extr)
   (make-instance 'vecteur-lié :origine-vect orig :extrémité-vect extr))
MAKE-VECT

Avant de définir une instance de vecteur-lié, disons vectAB, il nous faut définir les instances de point A et B:

(defparameter A (make-point 1 2 3))
A

(defparameter B (make-point 3 2 1))
B

(defparameter vectAB (make-vect A B))
VECTAB

vectAB
#<VECTEUR-LIÉ {100470E193}>
      on a bien obtenu un vecteur lié.

Si on veut avoir l'origine de vectAB:

(origine-de vectAB)
#<POINT {1004379BC3}>
On apprend seulement que c'est un point.

Vérifions ses coordonnées:

(coord-de (origine-de vectAB))
(1 2 3)
Ce sont bien les coordonnées du point A.

Vous vous rappelez de la fonction générique coord-de:
Nous avions défini une méthode coord-de pour la classe point.
Nous allons faire la même chose pour la classe vecteur-lié.
La définition de la méthode est très différente:

(defmethod coord-de ((obj vecteur-lié))
   (mapcar #'- (coord-de (extrémité-de obj)) (coord-de (origine-de obj))))
#<STANDARD-METHOD COMMON-LISP-USER::COORD-DE (VECTEUR-LIÉ) {1004CE3DE3}>

(coord-de vectAB)
(2 0 -2)
Nous obtenons bien les coordonnées (ou composantes) du vectAB défini comme ci-dessus.

Définissons un point C et le vecteur vectAC:

(defparameter C (make-point -1 2 4))
C

(defparameter vectAC (make-vect A C))
VECTAC

Voici la fonction produit-scalaire:

(defun produit-scalaire (vect1 vect2)
   (reduce #'+ (mapcar #'* (coord-de vect1) (coord-de vect2))))
PRODUIT-SCALAIRE

(produit-scalaire vectAB vectAC)
-6
C'est bien le produit scalaire des vecteurs vectAB et vectAC

On en déduit le carré scalaire d'un vecteur:

(defun carré-scalaire (vect)
   (produit-scalaire vect vect))
CARRÉ-SCALAIRE

(carré-scalaire vectAB)
8
C'est bien le carré scalaire du vecteur vectAB.

D'où le module d'un vecteur:

(defun module (vect)
   (sqrt (carré-scalaire vect)))
MODULE

(module vectAB)
2.828427
C'est bien cela.

Le cosinus de l'angle de deux vecteurs:

(defun cos-vect (vect1 vect2)
   (/ (produit-scalaire vect1 vect2) (* (module vect1) (module vect2))))
COS-VECT

(cos-vect vectAB vectAC)
-0.94868326

Somme de 2 vecteurs liés de même origine: à finir 

(defun somme-vect (vect1 vect2)
   "Rend un vecteur lié de même origine que les 2 arguments et égale à la somme vectorielle de ces 2 vecteurs."
   (if (eq (origine-de vect1) (origine-de vect2))
       (let ((liste (mapcar #'+ (coord-de (origine-de vect1)) (coord-de vect1) (coord-de vect2))))
(format t "Comment voulez-vous appeler l'extrémité du vecteur somme? ")
(setq pt (read))
(set pt (make-point (car liste) (cadr liste) (caddr liste)))
(format t "Comment voulez-vous appeler le vecteur somme? ")
(setq vect (read))
(set vect (make-vect A (eval pt))))
       (error "Les 2 vecteurs liés n'ont pas la même origine")))

(somme-vect vectAB vectAC)
Comment voulez-vous appeler l'extrémité du vecteur somme? D
Comment voulez-vous appeler le vecteur somme? vectAD

#<VECTEUR-LIÉ {100286E683}>
      On a ainsi construit le vecteur somme et son extrémité.

On peut, maintenant, obtenir les coordonnées du point D et les composantes du vecteur vectAD:

(coord-de D)
(1 2 2)

(coord-de vectAD)
(0 0 -1)

Le module de ce vecteur somme est:

(module vectAD)
1.0
Notez que c'est la longueur d'une diagonale du quadrilatère ABDC qui est un parallélogramme.

___________________________________

Construisons une nouvelle classe: sphère.
Une sphère est définie par son centre et son rayon.
Donc pour définir l'objet sphère nous utilisons 2 slots (ou spécificateurs): centre et rayon.
Pour faire simple, le rayon sera un nombre (on pourrait ajouter une unité et créer une classe grandeur).
Le centre est ... Un point! Nous allons utiliser la classe point vue précédemment.
C'est parti:

(defclass sphère (point)
   ((centre :accessor centre-sphère
    :writer change-centre
    :reader centre-de
    :initarg :centre-sph
    :initform (eval (defparameter O (make-point 0 0 0)))
    :type point
    :documentation "Le centre de la sphère.")
    (rayon :accessor rayon-sphère
   :writer change-rayon
   :reader rayon-de
   :initarg :rayon-sph
   :initform 1
   :type number
   :documentation "Le rayon de la sphère."))
   (:documentation "La classe de l'objet sphère."))
#<STANDARD-CLASS COMMON-LISP-USER::SPHÈRE>

On dit que "point" est une superclasse de sphère.
Définissons un constructeur:

(defun make-sphère (centre rayon)
   (make-instance 'sphère :centre-sph centre :rayon-sph rayon))
MAKE-SPHÈRE

Utilisons le point A (1 2 3) défini précédemment et un rayon de 10:

(defparameter spA (make-sphère A 10))
SPA
spA est la sphère de centre A et de rayon 10.

Profitons-en pour définir l'aire et le volume de la sphère:

(defun aire-sph (sph)
   (* 4 pi (expt (rayon-de sph) 2)))
AIRE-SPH

(aire-sph spA)
1256.6370614359173d0

(defun volume-sph (sph)
   (/ (* 4 pi (expt (rayon-de sph) 3)) 3))
VOLUME-SPH

(volume-sph spA)
4188.790204786391d0

J'espère que ce qui précède vous a donné des idées de développement.
Il y a beaucoup à faire dans ce domaine, même si on réinvente la roue.

Si vous devez redémarrer SLIME plusieurs fois, je vous suggère de recopier le fichier suivant.
Quand vous démarrez SLIME, vous tapez (load "votre chemin de répertoires/votre fichier").

;;;Début du fichier "votre fichier"
;;;Vous trouverez dans ce fichier un début de travail orienté objet dans CLOS (Common Lisp Object System)
;;;Vous devez avoir des notions de géométrie analytique dans l'espace.
;;;Si vous êtes débutant, je vous conseille de charger ce fichier dans SLIME sous EMACS.
;;;Puis de suivre le chapitre "classe d'objet" de mon blog.
;;;

;;Définition de la classe point:

(defclass point ()
   ((x :accessor abscisse-point ;pour obtenir l'abscisse du point ou la modifier
       :writer put-abs ;pour modifier l'abscisse
       :reader get-abs ;pour obtenir l'abscisse
       :reader x-de ;un 2ème reader plus pratique
       :initarg :x ;clé utilisée pour initialiser l'instance d'un point (son abscisse), sinon:
       :initform 0 ;valeur par défaut de l'abscisse
       :type number ;l'abscisse est du type number
       :documentation "L'abscisse du point.")     ;pour rappeler qu'on parle, ici, de l'abscisse
    (y :accessor ordonnée-point       ;même remarques pour le slot y que pour le slot x
       :writer put-ord
       :reader get-ord
       :reader y-de
       :initarg :y
       :initform 0
       :type number
       :documentation "L'ordonnée du point.")
    (z :accessor altitude-point ;même remarques pour le slot z que pour le slot x
       :writer put-alt
       :reader get-alt
       :reader z-de
       :initarg :z
       :initform 0
       :type number
       :documentation "L'altitude du point."))
   (:documentation "La classe de l'objet point.")) ;une documentation de la classe point

;;Définition d'un constructeur de point:

(defun make-point (x y z)
   (make-instance 'point :x x :y y :z z))

;;définition d'une fonction générique:

(defgeneric coord-de (obj)
   (:documentation "Rend les coordonnées de l'objet passé en argument."))

;;définition de la méthode pour l'objet point:

(defmethod coord-de ((obj point))
   (with-slots (x y z)
       obj
     (list x y z)))

;;définition de la classe vecteur-lié:

(defclass vecteur-lié (point)
   ((origine :accessor origine-vecteur
     :writer change-origine
     :reader origine-de
     :initarg :origine-vect
     :initform (eval (defparameter O (make-point 0 0 0)))
     :type point
     :documentation "L'origine du vecteur.")
    (extrémité :accessor extrémité-vecteur
       :writer change-extrémité
       :reader extrémité-de
       :initarg :extrémité-vect
       :initform (eval (defparameter I (make-point 1 0 0)))
       :type point
       :documentation "L'extrémité du vecteur."))
   (:documentation "La classe de l'objet vecteur-lié."))

;;définition d'un constructeur de vecteur-lié:

(defun make-vect (orig extr)
   (make-instance 'vecteur-lié :origine-vect orig :extrémité-vect extr))

;;méthode pour obtenir les composantes d'un vecteur:

(defmethod coord-de ((obj vecteur-lié))
   (mapcar #'- (coord-de (extrémité-de obj)) (coord-de (origine-de obj))))

;;fonction qui rend le produit scalaire de 2 vecteurs:

(defun produit-scalaire (vect1 vect2)
   (reduce #'+ (mapcar #'* (coord-de vect1) (coord-de vect2))))

;;fonction qui rend le carré scalaire d'un vecteur:

(defun carré-scalaire (vect)
   (produit-scalaire vect vect))

;;fonction qui rend le module d'un vecteur:

(defun module (vect)
   (sqrt (carré-scalaire vect)))

;;fonction qui rend le cosinus de deux vecteurs:

(defun cos-vect (vect1 vect2)
   (/ (produit-scalaire vect1 vect2) (* (module vect1) (module vect2))))

;;fonction permettant de construire la somme de deux vecteurs:

(defun somme-vect (vect1 vect2)
   "Rend un vecteur lié de même origine que les 2 arguments et égale à la somme vectorielle de ces 2 vecteurs."
   (if (eq (origine-de vect1) (origine-de vect2))
       (let ((liste (mapcar #'+ (coord-de (origine-de vect1)) (coord-de vect1) (coord-de vect2))))
(format t "Comment voulez-vous appeler l'extrémité du vecteur somme? ")
(setq pt (read))
(set pt (make-point (car liste) (cadr liste) (caddr liste)))
(format t "Comment voulez-vous appeler le vecteur somme? ")
(setq vect (read))
(set vect (make-vect A (eval pt))))
       (error "Les 2 vecteurs liés n'ont pas la même origine")))


;;Définition de la classe sphère:

(defclass sphère (point)
   ((centre :accessor centre-sphère
    :writer change-centre
    :reader centre-de
    :initarg :centre-sph
    :initform (eval (defparameter O (make-point 0 0 0)))
    :type point
    :documentation "Le centre de la sphère.")
    (rayon :accessor rayon-sphère
   :writer change-rayon
   :reader rayon-de
   :initarg :rayon-sph
   :initform 1
   :type number
   :documentation "Le rayon de la sphère."))
   (:documentation "La classe de l'objet sphère."))

;;Définition du constructeur de sphère:

(defun make-sphère (centre rayon)
   (make-instance 'sphère :centre-sph centre :rayon-sph rayon))

;;Pour le calcul de l'aire de la sphère:

(defun aire-sph (sph)
   (* 4 pi (expt (rayon-de sph) 2)))

;;Pour le calcul du volume de la sphère:

(defun volume-sph (sph)
   (/ (* 4 pi (expt (rayon-de sph) 3)) 3))



;;Fin du fichier



***>


____________________
Application aux comptes bancaires:


(defvar *numéros-de-compte* 0)
*NUMÉROS-DE-COMPTE* définition d'une variable, initialisée à 0, pour ce qui suit:

(defclass compte-bancaire () ;définie la classe "compte-bancaire"
  ((libellé                   ;première entrée
    :initarg :nom         ;initialisation du mot-clé de la première entrée
    :initform (error "Il faut un nom pour le libellé."))      ;message d'erreur si le libellé n'a pas de nom
   (solde                     ;deuxième entrée
    :initarg :solde ;initialisation du mot-clé de la deuxième entrée
    :initform 0)         ;la valeur de la deuxième entrée est initialisée à 0
   (numéro-compte ;troisième entrée
    :initform (incf *numéros-de-compte*)))) ;la valeur de la troisième entrée est incrémentée de 1
# rend le nom de la classe

MAKE-INSTANCE:

(defparameter *compte1*
  (make-instance 'compte-bancaire :nom "Truc" :solde 1000))
*COMPTE1*    crée une instance de compte-bancaire appelée *compte1* ayant pour                                   libellé "Truc" et pour solde 1000
 toute instance sans libellé entraîne une erreur
 si il y a un libellé et que le mot-clé :solde est omis, le solde prend la valeur 0, comme
 prévu dans l'initialisation de l'objet "compte-bancaire"

SLOT-VALUE: Vérification du contenu.

(slot-value *compte1* 'libellé)
"Truc" c'est bien la valeur du libellé

(slot-value *compte1* 'solde)
1000 c'est bien la valeur du solde

(slot-value *compte1* 'numéro-compte)
1 c'est bien le numéro de compte de cette instance
(*numéros-de-compte* avait été initialisée à 0, voir plus haut)
toute instance nouvelle aura un numéro-compte incrémenté par rapport à la                                         dernière instance

INITIALIZE-INSTANCE: est une fonction générique sur STANDARD-OBJECT.
La méthode primaire sur INITIALIZE-INSTANCE se charge d'initialiser les                                         entrées avec les options :initarg et :initform.
On peut définir une méthode :after spécialisée sur la classe en construction.
Par exemple, ajoutons une entrée dans la définition de la classe "compte-                                               bancaire":

(defclass compte-bancaire ()
  ((libellé
    :initarg :nom
    :initform (error "Il faut un nom pour le libellé."))
   (solde
    :initarg :solde
    :initform 0)
   (numéro-compte
    :initform (incf *numéros-de-compte*))
   type-compte)) ;une entrée ajoutée
#

(defmethod initialize-instance :after ((compte compte-bancaire) &key)
  (let ((solde (slot-value compte 'solde)))
    (setf (slot-value compte 'type-compte)
  (cond
    ((>= solde 100000) :or)
    ((>= solde 50000) :argent)
    (t :bronze)))))
# définition de la méthode :after

Remarque: &key est nécessaire dans la liste des paramètres pour être conforme avec la fonction générique. Ceci permet
 à certaines méthodes d'avoir leur mot-clés, sans obligation.

(defparameter *compte4*
  (make-instance 'compte-bancaire :nom "Bidule" :solde 150000))
*COMPTE4*   un nouveau compte est créé avec un solde de 150000

(slot-value *compte4* 'type-compte)
:OR l'entrée type-compte a bien pris la valeur :or

Si la méthode fait apparaître un paramètre &key, ce paramètre s'autorise dans la définition d'une instance. Exemple:

(defmethod initialize-instance :after ((compte compte-bancaire)
&key bonus-ouverture)
  (when bonus-ouverture
    (incf (slot-value compte 'solde)
  (* (slot-value compte 'solde) (/ bonus-ouverture 100)))))
#

(defparameter *compte5* (make-instance
 'compte-bancaire
 :nom "Jule"
 :solde 1000
 :bonus-ouverture 5)) ;nouvelle instance avec le paramètre :bonus-ouverture
*COMPTE5*

(slot-value *compte5* 'solde)
1050 le bonus-ouverture a bien été pris en compte.

Par contre:

(defparameter *compte6* (make-instance
 'compte-bancaire
 :nom "Jule"
 :solde 50))
*COMPTE6*             il n'y a pas de bonus-ouverture...

(slot-value *compte6* 'solde)
50 ...et le solde n'a pas changé

SLOT-VALUE permet l'accès aux différente entrées, mais, si on change la définition de l'objet, il peut être judicieux de créer une fonction qui pourra être modifiée à volonté. Par exemple, la fonction SOLDE:

(defun solde (compte)
  (slot-value compte 'solde))
SOLDE

(solde *compte6*)
50

Mais, si on est amené à définir des sous-classes de l'objet (compte-bancaire, ici), il sera préférable de créer une fonction générique qui pourra mener à différentes méthodes pour ces sous-classes, ou à des extensions avec des méthodes auxiliaires.
Par exemple:

(defgeneric solde1 (compte))
# définition de la fonction générique

(defmethod solde1 ((compte compte-bancaire)) ;la méthode est liée à la classe compte-bancaire
  (slot-value compte 'solde))
# définition de la méthode SOLDE1

(solde1 *compte6*)   ;un seul argument passé à SOLDE1
50 c'est bien le solde de *compte6*

Dans ce qui précède, la méthode permet de lire le solde du compte, mais évidemment pas de le changer!
On peut cependant être amené à changer le nom du compte. On utilise pour cela une fonction SETF étendue dont le nom est une liste de deux éléments: le 1er est setf et le 2ième est un symbole désignant la place où l'on veut un changement (ici, un changement de nom). Le nombre d'arguments est quelconque, mais le 1er est la valeur assignée à la place. Exemple:

(defun (setf libellé) (valeur compte)        ;attention de bien préciser l'entrée libellé
  (setf (slot-value compte 'libellé) valeur))
(SETF LIBELLÉ) la fonction setf étendue est définie

On l'utilise de la façon suivante:

(setf (libellé *compte6*) "Julo") ;l'entrée libellé doit apparaître de la même façon
"Julo"         le nouveau nom est rendu

(slot-value *compte6* 'libellé)
"Julo" simple vérification

Là, encore, il est préférable de créer une fonction générique et une méthode d'écriture:

(defgeneric (setf libellé) (valeur compte))
#
             la fonction précédente (SETF LIBELLé) a été remplacée par une fonction générique

(defmethod (setf libellé) (valeur (compte compte-bancaire))
  (setf (slot-value compte 'libellé) valeur))
#
                  ceci définit une méthode pour l'écriture d'une nouvelle valeur, dont voici l'appel:

(setf (libellé *compte6*) "Juleau")
"Juleau"

Voici, également, une fonction générique et une méthode de lecture:

(defgeneric libellé (compte))
#
                   définition de la fonction générique de lecture

(defmethod libellé ((compte compte-bancaire))
  (slot-value compte 'libellé))
#
                  définition de la méthode de lecture, dont voici l'appel:

(libellé *compte6*)
"Juleau"

Beaucoup mieux: les fonctions génériques et méthodes, de lecture ou d'écriture, peuvent être définies dans la définition de la classe de l'objet grâce aux options :reader, pour la lecture, :writer, pour l'écriture, et :accessor pour les deux.
L'option :documentation, permet de documenter les différentes entrées.
D'où une nouvelle définition de la classe compte-bancaire:

(defclass compte-bancaire ()
  ((libellé
    :initarg :nom
    :initform (error "Il faut un nom pour le libellé.")
    :accessor libellé
    :documentation "nom du titulaire du compte.")
   (solde
    :initarg :solde
    :initform 0
    :reader solde
    :documentation "solde du compte.")
   (numéro-compte
    :initform (incf *numéros-de-compte*)
    :reader numéro-compte
    :documentation "numéro de compte unique en banque.")
   (type-compte
    :reader type-compte
    :documentation "type de compte: or, argent ou bronze.")))
#

WITH-SLOTS: macro permettant un accès direct aux entrées d'une classe. La forme basique est:
(with-slots (slot*) instance-form
 body-form*)
slot* peut être le nom de l'entrée qui est utilisé aussi comme variable, ou bien une liste dont le premier élément est une variable qui prend la valeur de l'entrée et le deuxième le nom de l'entrée.
instance-form est le nom de l'instance de l'objet et est évalué une seule fois.
Dans le corps body-form*, chaque occurrence de la variable définie dans slot* se traduit par un appel à SLOT-VALUE sans qu'il soit nécessaire de préciser à chaque fois l'instance d'objet et l'entrée.
Exemple:

(defparameter *solde-minimum* 100)
*SOLDE-MINIMUM* définit la variable *solde-minimum* avec la valeur 100

(defmethod pénalité-pour-solde-faible ((compte compte-bancaire))
  (with-slots (solde) compte
    (when (< solde *solde-minimum*)
      (decf solde (* solde .01)))))
#       définition de la méthode utilisant WITH-SLOTS

(pénalité-pour-solde-faible *compte6*)
49.5 bien noter la forme de l'appel à la méthode

Voici la 2ème forme de WITH-SLOTS:

(defmethod pénalité-pour-solde-faible2 ((compte compte-bancaire))
  (with-slots ((so solde)) compte
    (when (< so *solde-minimum*)
      (decf so (* so .01)))))
#

(pénalité-pour-solde-faible2 *compte6*)
49.005 la pénalité s'est bien appliquée à nouveau

WITH-ACCESSORS: macro permettant de raccourcir les méthodes faisant intervenir l'option :accessor de certaines entrées.

Par exemple, si le libellé de l'objet précédent utilisait l'option :accessor au lieu de :reader, on pourrait construire la méthode suivante pour virer un compte sur un autre:

(defmethod virement ((compte1 compte-bancaire) (compte2 compte-bancaire))
  (with-accessors ((solde1 solde)) compte1
    (with-accessors ((solde2 solde)) compte2
      (incf solde1 solde2)
      (setf solde2 0))))

L'appel à la méthode serait (non vérifié):

(virement *compte5* *compte6*) le solde du compte6 est viré sur le compte5, puis le solde du                                                                   compte6 est mis à zéro

:allocation est une autre option d'entrée qui peut prendre pour valeur :instance (par défaut) ou :class.
Si la valeur est :class, l'entrée a une valeur unique stockée dans la classe et partagée par les instances.
Même si elle n'est pas stockée dans l'instance, on accède à cette valeur en passant par l'instance.

Dans ce qui suit bar est une sous-classe de la classe foo:

(defclass foo ()
  ((a :initarg :a :initform "A" :accessor a)
   (b :initarg :b :initform "B" :accessor b)))
#

(defclass bar (foo)
  ((a :initform (error "Il faut fournir une valeur à a"))
   (b :initarg :le-b :accessor le-b :allocation :class)))
#

(defparameter *un* (make-instance
    'bar
    :le-b "béta"))
; Evaluation aborted on #.
       la sous-classe bar impose de fournir une valeur à a lors de l'instanciation
  l'option :initform de bar a écrasé celle héritée de foo

(defparameter *un* (make-instance
    'bar
    :a "alpha"
    :le-b "béta"))
*UN* l'instance est, cette fois, bien définie

(slot-value *un* 'a)
"alpha" l'entrée a a bien la valeur "alpha"

(slot-value *un* 'b)
"béta" l'entrée b a bien la valeur "béta"

(defparameter *deux* (make-instance
    'bar
    :a "alpha2"))
*DEUX* pour cette nouvelle instance, la valeur de b n'est pas fournie

(slot-value *deux* 'a)
"alpha2"         la valeur de a est bien la valeur fournie: alpha2

(slot-value *deux* 'b)
"béta" b a pris la valeur fournie par la 1ère instanciation, ceci grâce à                                                             l'option :allocation qui a, ici, la valeur :class. La valeur de b est                                                             stockée dans la classe bar et est partagée par toutes les instances de bar.

La prochaine fois: FORMAT: les bases et quelques recettes.

14-STRUCTURES.

>>>>
Liste des symboles rencontrés dans ce chapitre:
DEFSTRUCT MAKE-ESSAI ESSAI-CHAMP1 ESSAI-P COPY-ESSAI SYMBOL-FUNCTION 

Structures.

DEFSTRUCT:

(defstruct essai champ1 champ2)
ESSAI définit une structure contenant 2 champs

Quand on définit une structure, on crée, en même temps, des fonctions sur cette structure.
Pour cette structure essai, on a donc les fonctions:
MAKE-ESSAI, ESSAI-P, COPY-ESSAI, ESSAI-CHAMP1, ESSAI-CHAMP2.

MAKE-ESSAI:     (fonction construite par DEFSTRUCT)

(setf s (make-essai :champ1 5))
#S(ESSAI :CHAMP1 5 :CHAMP2 NIL) make-essai crée une instance de essai avec, ici, champ1                                                                         initialisé à 5
                            l'instance prend le nom de S.

ESSAI-CHAMP1:  (fonction construite par DEFSTRUCT)

(essai-champ1 s)
5 rend le contenu du champ1 de S

(setf (essai-champ2 s) "OK")
"OK" S étant déjà définit, complète ainsi le champ2

s
#S(ESSAI :CHAMP1 5 :CHAMP2 "OK") vérification

Avec la structure, se crée aussi un type du même nom:

ESSAI-P:        (fonction construite par DEFSTRUCT: prédicat vérifiant le type de la structure créée)

(essai-p s)
T                 S est bien du type essai

ou bien:

(typep s 'essai)
T

COPY-ESSAI:     (fonction construite par DEFSTRUCT)

(defparameter s1 (copy-essai s))
S1       s1 contient une copie de s

s1
#S(ESSAI :CHAMP1 5 :CHAMP2 "OK") s1 a la même valeur que s

(essai-p s1)
T s1 est bien du type essai

_________________________________________________________________
FERMETURES:
ou CLOSURES:

Utilisation de fermetures dans la définition d'une structure.

Exemple de fermeture:

La fonction nouvel-arg, ci-dessous, est une fermeture qui rend NIL la première fois.
Si l'argument passé est supérieur à l'argument précédent, elle rend T.
Sinon, elle rend NIL.

(let ((a 0) (b 0))
   (defun nouvel-arg (x)
     (if (not (numberp x)) (error "L'argument doit être un nombre.")
(progn
   (cond ((= b 0) (incf b) (setf a x) nil)
((> x a) (setf a x) t)
(t (setf a x) nil))))))

La fermeture arg-le+grand rend le plus grand argument qui lui a été passé jusquà maintenant.
La fermeture init réinitialise cet "argument" à zéro.

(let ((a 0))
   (defun arg-le+grand (x)
     (if (> x a) ;si x>a...
(setf a x) ;...l'évaluation place la valeur de x dans a, puis rend cette valeur
a))    ;sinon la valeur de a est rendue
   (defun init ()
     (setf a 0)))

Je vous laisse les essayer.
En voici un autre exemple qu 'on utilisera plus loin.

(let ((i 0))
(defun suivant () ;la fonction suivant, définie dans le let, est une fermeture qui...
(incf i))   ;...incrémente i
(defun réinit () ;réinit est, aussi, une fermeture qui...
(setf i 0)))         ;...réinitialise i

(suivant)
1 rend la valeur de i après incrémentation

(réinit)
0 rend la valeur de i après réinitialisation

SYMBOL-FUNCTION:

Remarque: suivant et réinit ne sont pas des fonctions locales. Ce sont des fonctions globales avec un environnement, en partie, fermé.
On peut modifier l'association d'un symbole et d'une fermeture avec SYMBOL-FUNCTION qui est setf-able.
Mais attention de conserver la fermeture si on veut la réutiliser par la suite:

(suivant)
1 après la réinitialisation précédente SUIVANT rend 1

(defparameter *ferm* (symbol-function 'suivant))
*FERM*      la fonction associée à SUIVANT est sauvegardée dans la variable *ferm*

(setf (symbol-function 'suivant) #'(lambda (i) (incf i 5)))
#<FUNCTION (LAMBDA (I)) {B7DE34D}>   SUIVANT est associé à une autre fonction qui incrémente de 5 l'argument i

(suivant 3)
8 l'appel de la fonction SUIVANT avec l'argument 3 rend 8 (3+5)
sans argument, il y aurait une erreur.

(setf (symbol-function 'suivant) *ferm*)
#<CLOSURE SUIVANT>     SUIVANT est associé à l'ancienne fermeture

(suivant)
2     i est de nouveau incrémenté

On réinitialise la fonction suivant pour la suite:

(réinit)
0

Voici une structure qui utilise la fermeture suivant et qui définit d'autres fermetures:

(defstruct inscription
  (numéro (suivant)) ;le champ numéro utilise la fermeture suivant
  (nom (progn ;création d'une nouvelle fermeture utilisée lors de la création d'une instance d'inscription
 (format t "Quel est votre nom?~%")  ;le nom est demandé à la création de l'instance
 (read)))              ;le champ nom prend la valeur lue
  (prénom (progn  ;nouvelle fermeture
    (format t "Quel est votre prénom?~%")
    (read)))
  (naiss (progn  ;nouvelle fermeture
   (format t "quelle est votre année de naissance?~%")
   (read)))
  (email (progn  ;nouvelle fermeture
   (format t "indiquez votre adresse de courriel:~%")
   (read))))

(setf j1 (make-inscription))
Quel est votre nom?Dupont il peut y avoir un message d'erreur si j1 n'a pas été défini précédemment
Quel est votre prénom?Alain
quelle est votre année de naissance?1954
indiquez votre adresse de courriel:dupont.alain@free.fr

#S(INSCRIPTION
   :NUMÉRO 1 1er enregistrement
   :NOM DUPONT
   :PRÉNOM ALAIN
   :NAISS 1954
   :EMAIL DUPONT.ALAIN@FREE.FR)

(setf j2 (make-inscription))
Quel est votre nom?Dupond
Quel est votre prénom?Jean
quelle est votre année de naissance?1957
indiquez votre adresse de courriel:dupond.jean@laposte.fr

#S(INSCRIPTION
   :NUMÉRO 2 2ème enregistrement: il y a bien eu incrémentation
   :NOM DUPOND
   :PRÉNOM JEAN
   :NAISS 1957
   :EMAIL DUPOND.JEAN@LAPOSTE.FR)

Pour avoir le nom de la personne j1:

(inscription-nom j1)
DUPONT

Si vous trouvez que inscription- est trop long à écrire, redéfinissez la structure de la façon suivante:

(defstruct (inscription (:conc-name ins-))
   (numéro (suivant))
   ....etc

Et, une fois l'instance j1 définie:

(ins-prénom personne1)
ALAIN

Vous pouvez entrer des données au moment de la définition de l'instance.
Redéfinissons, d'abord la structure:

(defstruct (personne (:conc-name ins-))
   civilité
   liste
   (numéro (suivant)) ;le champ numéro utilise la fermeture suivant
   (nom (progn ;création d'une nouvelle fermeture utilisée lors de la création d'une instance d'inscription
  (format t "Quel est votre nom?~%")   ;le nom est demandé à la création de l'instance
  (read)))              ;le champ nom prend la valeur lue
   (prénom (progn   ;nouvelle fermeture
     (format t "Quel est votre prénom?~%")
     (read)))
   (naiss (progn   ;nouvelle fermeture
    (format t "quelle est votre année de naissance?~%")
    (read)))
   (email (progn   ;nouvelle fermeture
    (format t "indiquez votre adresse de courriel:~%")
    (read))))

La structure se nomme, maintenant, PERSONNE.
Nous avons deux nouveaux slots (emplacements): civilité et liste.
Nous pouvons définir une instance en précisant la civilité (M ou Mme) et la liste (rouge ou verte):

(setf personne2 (make-personne :civilité 'Mme :liste 'verte))

Vous voyez un message d'erreur si personne2 n'a pas été initialisée.
Vous remplissez le questionnaire et vous obtenez par exemple:

#S(PERSONNE
   :CIVILITÉ MME
   :LISTE VERTE ;Mme et verte sont des données saisies avant la définition de l'instance PERSONNE2
   :NUMÉRO 2 ;le reste est saisie pendant la définition de l'instance.
   :NOM DURAND
   :PRÉNOM SOPHIE
   :NAISS 1960
   :EMAIL DURANDSOPHIE2@ORANGE.FR)

A partir d'ici, je rajoute ce que dit l'HyperSpec sur defstruct, mais en français.
Et, à quelques détails près.
N'hésitez pas à me signaler dans vos commentaires les erreurs de traduction que j'aurais pu commettre.
Attention, accrochez-vous!
Vous allez retrouver les exemples précédents. Vous aurez l'impression d'une redite. Mais les explications sont plus compliquées.
Si vous préférez sauter ce passage Hyperspec, faites une recherche en avant sur le trait suivant:
__________________________________

DEFSTRUCT (langage LISP).

Syntaxe:
defstruct name-and-options [documentation] {slot-description}*


Description:

defstruct définit un type structuré, nommé structure-type, avec des slots nommés comme spécifié par les slots-options.

defstruct définit les lecteurs (readers) pour les emplacements et fait en sorte que setf fonctionne correctement sur ces fonctions de lecteur (reader). En outre, sauf en cas de substitution, il définit un prédicat nommé nom-p, définit une fonction constructeur nommée make-constructeur-nom et définit une fonction copieur nommée copie-constructeur-nom. Tous les noms des fonctions créées automatiquement peuvent être automatiquement déclarés en ligne (à la discrétion de l'implémentation).

Exemple :

(defstruct essai "C'est la doc de ce type." champ1 champ2)
ESSAI définit une structure (un type) contenant 2 champs

essai est du type « type », donc sa doc s’obtient de la façon suivante :

(documentation 'essai 'type)
"C'est la doc de ce type."

(setf s (make-essai :champ1 5))
#S(ESSAI :CHAMP1 5 :CHAMP2 NIL) make-essai crée une instance de essai avec, ici, champ1 initialisé à 5
        l'instance prend le nom de S.

(essai-champ1 s)
5 rend le contenu du champ1 de S

(setf (essai-champ2 s) "OK")
"OK" S étant déjà définit, complète ainsi le champ2

s
#S(ESSAI :CHAMP1 5 :CHAMP2 "OK") vérification

Avec la structure, se crée aussi un type du même nom:

(essai-p s)
T L’instance S est du type essai

(defparameter s1 (copy-essai s))
S1       s1 contient une copie de s

s1
#S(ESSAI :CHAMP1 5 :CHAMP2 "OK") s1 a la même valeur que s

(essai-p s1)
T s1 est bien du type essai

Si la documentation est fournie, elle est attachée à structure-name en tant que chaîne de documentation de type structure, et sauf si :type est utilisé, la documentation est également attachée à structure-name en tant que chaîne de documentation de type type et en tant que chaîne de documentation à l’objet classe pour la classe nommée nom-structure.

defstruct définit une fonction constructeur qui est utilisée pour créer des instances de la structure créée par defstruct. Le nom par défaut est make-structure-name. Un nom différent peut être fourni en donnant le nom comme argument à l'option constructeur. nil indique qu'aucune fonction constructeur ne sera créée.

Une fois qu'un nouveau type de structure a été défini, des instances de ce type peuvent normalement être créées en utilisant la fonction constructeur pour le type. Un appel à une fonction constructeur a la forme suivante:

(nom-fonction-constructeur
  slot-keyword-1 form-1
  slot-keyword-2 form-2
  …)

Exemple :

(defstruct inscription
   (numéro (suivant)) ;le champ numéro utilise la fermeture suivant
   (nom (progn ;création d'une nouvelle fermeture utilisée lors de la création d'une instance d'inscription
  (format t "Quel est votre nom?~%")   ;le nom est demandé à la création de l'instance
  (read)))              ;le champ nom prend la valeur lue
   (prénom (progn   ;nouvelle fermeture
     (format t "Quel est votre prénom?~%")
     (read)))
   (naiss (progn   ;nouvelle fermeture
    (format t "quelle est votre année de naissance?~%")
    (read)))
   (email (progn   ;nouvelle fermeture
    (format t "indiquez votre adresse de courriel:~%")
    (read))))

note : suivant est une fermeture qui doit être définie avant et qui rend un nouveau numéro non utilisé.

Les arguments de la fonction constructeur sont tous des arguments de mots clés. Chaque argument de mot clé d'emplacement (slot) doit être un mot clé dont le nom correspond au nom d'un emplacement de structure. Tous les mots-clés et formes sont évalués. Si un slot n'est pas initialisé de cette manière, il est initialisé en évaluant slot-initform dans la description du slot au moment où la fonction constructeur est appelée. Si aucun slot-initform n'est fourni, les conséquences ne sont pas définies si une tentative est faite ultérieurement pour lire la valeur du slot avant qu'une valeur ne soit explicitement affectée.

Chaque slot-initform fourni pour un composant defstruct, lorsqu'il est utilisé par la fonction constructeur pour un composant par ailleurs non fourni, est réévalué à chaque appel à la fonction constructeur. Le slot-initform n'est pas évalué à moins qu'il ne soit nécessaire dans la création d'une instance de structure particulière. S'il n'est jamais nécessaire, il ne peut y avoir d'erreur de non-concordance de type, même si le type de l'emplacement est spécifié; aucun avertissement ne doit être émis dans ce cas. Par exemple, dans la séquence suivante, seul le dernier appel est une erreur.

(defstruct person (name 007 :type string)) 
 (make-person :name "James")
 (make-person)
C'est comme si les slot-initforms étaient utilisés comme formes d'initialisation pour les paramètres de mot-clé de la fonction constructeur.

Les symboles qui nomment les emplacements ne doivent pas être utilisés par l'implémentation comme noms pour les variables lambda dans la fonction constructeur, car un ou plusieurs de ces symboles peuvent avoir été proclamés spéciaux ou peuvent être définis comme le nom d'une variable constante. Les formes init par défaut de slot sont évaluées dans l'environnement lexical dans lequel la forme defstruct elle-même apparaît et dans l'environnement dynamique dans lequel apparaît l'appel à la fonction constructeur.

Par exemple, si la forme (gensym) était utilisé comme forme d'initialisation, soit dans l'appel de fonction constructeur ou comme forme d'initialisation par défaut dans defstruct, alors chaque appel à la fonction constructeur appellerait Gensym une fois pour générer un nouveau symbole.

Chaque description d'emplacement dans defstruct peut spécifier zéro ou plusieurs options d'emplacement. Une slot-option se compose d'une paire d'un mot-clé et d'une valeur (qui n'est pas une forme à évaluer, mais la valeur elle-même). Par exemple:

(defstruct ship
   (x-position 0.0 :type short-float)
   (y-position 0.0 :type short-float)
   (x-velocity 0.0 :type short-float)
   (y-velocity 0.0 :type short-float)
   (mass *default-ship-mass* :type short-float :read-only t))
Ceci spécifie que chaque slot (emplacement) contient toujours un nombre flottant court et que le dernier slot ne peut pas être modifié une fois qu'une instance de ship est construite.

Les options de slot disponibles sont:

: type type

Cela spécifie que le contenu de l'emplacement est toujours de type type. Ceci est entièrement analogue à la déclaration d'une variable ou d'une fonction; il déclare effectivement le type de résultat de la fonction reader. Cela dépend de l'implémentation, que le type soit vérifié lors de l'initialisation d'un emplacement ou lors de son affectation. Le type n'est pas évalué; il doit s'agir d'un spécificateur de type valide.

:read-only x

Les options  disponibles de ce slot sont:

Lorsque x est T (vrai), cela spécifie que cet emplacement ne peut pas être modifié; il contiendra toujours la valeur fournie au moment de la construction. setf n'acceptera pas la fonction du reader pour cet emplacement. Si x est faux, cette option de slot n'a aucun effet. X n'est pas évalué.

Lorsque cette option est fausse ou non fournie, elle dépend de l'implémentation, que la capacité d'écrire le slot soit implémentée par une fonction setf ou un expanseur setf.

  Les options de mots clés suivantes sont disponibles pour une utilisation avec defstruct. Une option defstruct peut être soit un mot clé, soit une liste de mots clés et d'arguments pour ce mot clé; la spécification du mot-clé par lui-même équivaut à spécifier une liste composée du mot-clé et d'aucun argument. La syntaxe des options defstruct diffère de la syntaxe de paire utilisée pour les options de slot. Aucune partie de ces options n'est évaluée.

:conc-name

Cela permet de préfixer automatiquement les noms des fonctions de lecture (ou d'accès). Le comportement par défaut consiste à commencer les noms de toutes les fonctions de lecture d'une structure par le nom de la structure suivi d'un trait d'union.
Exemple : essai-champ1.

: conc-name fournit un préfixe alternatif à utiliser. Si un trait d'union doit être utilisé comme séparateur, il doit être fourni dans le cadre du préfixe. Si :conc-name est nul ou si aucun argument n'est fourni, aucun préfixe n'est utilisé; les noms des fonctions du lecteur sont alors les mêmes que les noms des emplacements. Si un préfixe non nul est donné, le nom de la fonction de lecture pour chaque emplacement est construit en concaténant ce préfixe et le nom de l'emplacement, et en internant le symbole résultant dans le package qui est actuel au moment où la forme de défstructure est développée : c’est à dire que le symbole devient « interned » dans le package courant.

Notez que peu importe ce qui est fourni pour :conc-name, les mots-clés d'emplacement qui correspondent aux noms d'emplacement sans aucun préfixe attaché sont utilisés avec une fonction constructeur. Le nom de la fonction de lecture est utilisé conjointement avec setf. Voici un exemple:

(defstruct (door (:conc-name dr-)) knob-color width material) =>  DOOR
 (setq my-door (make-door :knob-color 'red :width 5.0)) 
=>  #S(DOOR :KNOB-COLOR RED :WIDTH 5.0 :MATERIAL NIL)
 (dr-width my-door) =>  5.0
 (setf (dr-width my-door) 43.7) =>  43.7
 (dr-width my-door) =>  43.7
note : knob signifie bouton
Que l'option :conc-name soit explicitement fournie ou non, la règle suivante régit les conflits de noms des lecteurs (ou accesseurs) générés: pour tout type de structure S1 ayant une fonction de lecteur nommée R pour un emplacement nommé X1 hérité par une autre structure type S2 qui aurait une fonction de lecture du même nom R pour un slot nommé X2, aucune définition de R n'est générée par la définition de S2; à la place, la définition de R est héritée de la définition de S1. (Dans un tel cas, si X1 et X2 sont des emplacements différents, la mise en œuvre peut signaler un avertissement de style.)

:constructor

Cette option prend zéro, un ou deux arguments. Si au moins un argument est fourni et que le premier argument n'est pas nul, alors cet argument est un symbole qui spécifie le nom de la fonction constructeur. Si l'argument n'est pas fourni (ou si l'option elle-même n'est pas fournie), le nom du constructeur est produit en concaténant la chaîne "MAKE-" et le nom de la structure, en internant le nom dans le package courant à ce moment defstruct est élargi. Si l'argument est fourni et est NIL, aucune fonction constructeur n'est définie.

Si :constructor est donné comme (:constructor arglist), alors au lieu de créer une fonction constructeur pilotée par mot-clé, defstruct définit une fonction constructeur « positionnelle », en prenant des arguments dont la signification est déterminée par la position de l'argument et éventuellement par des mots clés. Arglist est utilisé pour décrire quels seront les arguments du constructeur. Dans le cas le plus simple, quelque chose comme (:constructor make-foo (a b c)) définit make-foo comme une fonction constructeur à trois arguments dont les arguments sont utilisés pour initialiser les emplacements nommés a, b et c.

Parce qu'un constructeur de ce type fonctionne « par ordre d'arguments » (By Order of Arguments), il est parfois appelé « constructeur boa ».

Pour plus d'informations sur le traitement de l'arglist d'un « constructeur boa », voir la section 3.4.6 de Common Lisp HyperSpec (Boa Lambda Lists).

Il est permis d'utiliser l'option: constructeur plusieurs fois, afin de pouvoir définir plusieurs fonctions constructeur différentes, chacune prenant des paramètres différents.

defstruct crée la fonction constructeur de mot-clé par défaut uniquement si aucune option explicite: constructeur n'est spécifiée, ou si l'option :constructor est spécifiée sans argument de nom.

(:constructor nil) n'a de sens que si aucune autre option :constructor n'est spécifiée. Il empêche le defstruct de générer des constructeurs.

Sinon, defstruct crée une fonction constructeur correspondant à chaque option constructeur fournie. Il est permis de spécifier plusieurs fonctions de constructeur de mots clés ainsi que plusieurs 
«constructeurs boa ».

:copier

 Cette option prend un argument, un symbole, qui spécifie le nom de la fonction copieur. Si l'argument n'est pas fourni ou si l'option elle-même n'est pas fournie, le nom du copieur est produit en concaténant la chaîne "COPY-" et le nom de la structure, en internant le nom dans le package courant au moment où la structure est étendu. Si l'argument est fourni et est nul, aucune fonction de copieur n'est définie. La fonction de copieur définie automatiquement est fonction d'un argument, qui doit être du type de structure défini. La fonction copieur crée une nouvelle structure qui a le même type que son argument et qui a les mêmes valeurs de composant que la structure d'origine; c'est-à-dire que les valeurs des composants ne sont pas copiées récursivement. Si l'option defstruct: type n'a pas été utilisée, l'équivalence suivante s'applique:

(copier-nom x) = (copy-structure (la structure-nom x))


:include

 Cette option est utilisée pour créer une nouvelle définition de structure en tant qu'extension d'une autre définition de structure. Par exemple: 

(defstruct person name age sex)

 Pour créer une nouvelle structure pour représenter un astronaute qui a les attributs de nom, d'âge et de sexe, et des fonctions qui opèrent sur les structures de la personne, l'astronaute est défini avec:

 (defstruct (astronaut (:include person)
                           (:conc-name astro-))
        helmet-size
        (favorite-beverage 'tang))

:include fait en sorte que la structure définie ait les mêmes emplacements que la structure incluse. Ceci est fait de telle manière que les fonctions de lecture de la structure incluse fonctionnent également sur la structure en cours de définition. Dans cet exemple, un astronaute a donc cinq emplacements: les trois définis par la structure person et les deux définis par la structure astronaut lui-même. Les fonctions de lecteur définies par la structure person peuvent être appliquées aux instances de la structure astronaut et fonctionnent correctement. De plus, l'astronaute a ses propres fonctions de lecture pour les composants définis par la structure person. Les exemples suivants illustrent l'utilisation de la structure astronaut:

 (setq x (make-astronaut :name 'buzz
                         :age 45.
                         :sex t
                         :helmet-size 17.5))
 (person-name x) =>  BUZZ
 (astro-name x) =>  BUZZ
 (astro-favorite-beverage x) =>  TANG
 (reduce #'+ astros :key #'person-age) ; obtains the total of the ages 
                                       ; of the possibly empty
                                       ; sequence of astros
La différence entre les fonctions lecteur person-name et astro-name est que person-name peut être correctement appliqué à toute personne, y compris un astronaute, tandis que le astro-name ne peut être correctement appliqué qu'à un astronaute. Une implémentation peut vérifier l'utilisation incorrecte des fonctions de lecture.

Au plus un :include peut être fourni dans un seul bâti. L'argument pour :include est obligatoire et doit être le nom d'une structure précédemment définie. Si la structure en cours de définition n'a pas d'option :type, alors la structure incluse doit également n’avoir fourni aucune option :type. Si la structure en cours de définition a une option :type, alors la structure incluse doit avoir été déclarée avec une option :type spécifiant le même type de représentation.

Si aucune option :type n’est impliquée, le nom de structure de la définition de structure incluse devient le nom d'un type de données, et donc un spécificateur de type valide reconnaissable par typep; il devient un sous-type de la structure incluse. Dans l'exemple ci-dessus, l'astronaute est un sous-type de personne; Par conséquent

 (typep (make-astronaut) 'person) =>  true
indiquant que toutes les opérations sur les personnes fonctionnent également sur les astronautes.

La structure utilisant :include peut spécifier des valeurs par défaut ou des options de slot pour les slots inclus différents de ceux spécifiés par la structure incluse, en donnant l'option :include de la façon suivante:

 (:include included-structure-name slot-description*)
Chaque description d'emplacement doit avoir un nom d'emplacement identique à celui de certains emplacements dans la structure incluse. Si une description de slot n'a pas de forme init de slot, alors dans la nouvelle structure le slot n'a pas de valeur initiale. Sinon, sa forme de valeur initiale est remplacée par le slot-initform dans la description du slot. Un emplacement normalement accessible en écriture peut être rendu en lecture seule. Si un emplacement est en lecture seule dans la structure incluse, il doit également l'être dans la structure incluse. Si un type est fourni pour un emplacement, il doit s'agir d'un sous-type du type spécifié dans la structure incluse.

Par exemple, si l'âge par défaut d'un astronaute est 45 ans, alors

 (defstruct (astronaut (:include person (age 45)))
    helmet-size
    (favorite-beverage 'tang))
Si :include est utilisé avec l'option :type, l'effet consiste d'abord à ignorer autant d'éléments de représentation que nécessaire pour représenter la structure incluse, puis à ignorer tous les éléments supplémentaires fournis par l'option :initial-offset, puis à commencer l'allocation des éléments à partir de ce point. Par exemple:

(defstruct (binop (:type list) :named (:initial-offset 2))
   (operator '? :type symbol)   
   operand-1
   operand-2) =>  BINOP
 (defstruct (annotated-binop (:type list)
                             (:initial-offset 3)
                             (:include binop))
  commutative associative identity) =>  ANNOTATED-BINOP
 (make-annotated-binop :operator '*
                       :operand-1 'x
                       :operand-2 5
                       :commutative t
                       :associative t
                       :identity 1)
   =>  (NIL NIL BINOP * X 5 NIL NIL NIL T T 1)
Les deux premiers éléments nuls découlent du décalage initial de 2 dans la définition de binop. Les quatre éléments suivants contiennent le nom de la structure et trois emplacements pour binop. Les trois éléments nuls suivants découlent du décalage initial de 3 dans la définition de binop annoté. Les trois derniers éléments de la liste contiennent les emplacements supplémentaires pour un binop annoté.

:initial-offset

:initial-offset demande à defstruct de sauter un certain nombre de slots avant de commencer à allouer les slots décrits dans le corps. L'argument de cette option est le nombre d'emplacements que la structure doit ignorer. :initial-offset ne peut être utilisé que si :type est également fourni.

:initial-offset permet d'allouer des tranches à partir d'un élément de représentation autre que le premier. Par exemple, le formulaire

 (defstruct (binop (:type list) (:initial-offset 2))
   (operator '? :type symbol)
   operand-1
   operand-2) =>  BINOP
entraînerait le comportement suivant pour make-binop:

 (make-binop :operator '+ :operand-1 'x :operand-2 5)
=>  (NIL NIL + X 5)
 (make-binop :operand-2 4 :operator '*)
=>  (NIL NIL * NIL 4)
Les fonctions de sélection binop-operator, binop-operand-1 et binop-operand-2 seraient essentiellement équivalentes aux troisième, quatrième et cinquième, respectivement. De même, le formulaire

(defstruct (binop (:type list) :named (:initial-offset 2))
   (operator '? :type symbol)
   operand-1
   operand-2) =>  BINOP
entraînerait le comportement suivant pour make-binop:

(make-binop :operator '+ :operand-1 'x :operand-2 5) =>  (NIL NIL BINOP + X 5)
 (make-binop :operand-2 4 :operator '*) =>  (NIL NIL BINOP * NIL 4)
Les deux premiers éléments nuls découlent du décalage initial de 2 dans la définition de binop. Les quatre éléments suivants contiennent le nom de la structure et trois emplacements pour binop.

:named

   :named spécifie que la structure est nommée. Si aucun :type n’est fourni, la structure est toujours nommée.

Par exemple:

 (defstruct (binop (:type list))
   (operator '? :type symbol)
   operand-1
   operand-2) =>  BINOP
Ceci définit une fonction constructeur make-binop et trois fonctions de sélection, à savoir binop-operator, binop-operand-1 et binop-operand-2. (Il ne définit cependant pas de prédicat binop-p, pour les raisons expliquées ci-dessous.)

L'effet de make-binop est simplement de construire une liste de longueur trois:

(make-binop :operator '+ :operand-1 'x :operand-2 5) =>  (+ X 5)  
 (make-binop :operand-2 4 :operator '*) =>  (* NIL 4)
Elle est similaire à la liste de fonctions, sauf qu'elle prend des arguments de mots clés et effectue une valeur par défaut de slot appropriée au type de données conceptuel binop. De même, les fonctions de sélection binop-operator, binop-operand-1 et binop-operand-2 sont essentiellement équivalentes à car, cadr et caddr, respectivement. Ils peuvent ne pas être complètement équivalents car, par exemple, une implémentation serait justifiée en ajoutant du code de vérification des erreurs pour garantir que l'argument de chaque fonction de sélecteur est une liste de longueur 3.

binop est un type de données conceptuel en ce qu'il n'est pas intégré au système de type Common Lisp. typep ne reconnaît pas binop en tant que spécificateur de type, et le type de liste de retour lorsqu'il reçoit une structure binop. Il n'y a aucun moyen de distinguer une structure de données construite par make-binop de toute autre liste qui se trouve avoir la structure correcte.

Il n'y a aucun moyen de récupérer le nom de la structure binop à partir d'une structure créée par make-binop. Cela ne peut être fait que si la structure est nommée. Une structure nommée a la propriété que, étant donné une instance de la structure, le nom de la structure (qui nomme le type) peut être récupéré de manière fiable. Pour les structures définies avec aucune option :type, le nom de la structure fait partie du système de type de données Common Lisp. type-of, lorsqu'il est appliqué à une telle structure, renvoie le nom de la structure comme type de l'objet; typep reconnaît le nom de la structure comme un spécificateur de type valide.

Pour les structures définies avec une option :type, type-of renvoie un spécificateur de type tel que list ou (vecteur t), selon le type fourni à l'option :type. Le nom de la structure ne devient pas un spécificateur de type valide. Cependant, si l'option :named est également fournie, le premier composant de la structure (créé par une fonction constructeur defstruct) contient toujours le nom de la structure. Cela permet de récupérer le nom de la structure à partir d'une instance de la structure et de définir un prédicat raisonnable pour le type conceptuel: le prédicat name-p défini automatiquement pour la structure fonctionne en vérifiant d'abord que son argument est du type approprié ( liste, (vecteur t) ou autre), puis vérifier si le premier composant contient le nom de type approprié.

Considérez l'exemple binop ci-dessus, modifié uniquement pour inclure l'option :named:

(defstruct (binop (:type list) :named)
   (operator '? :type symbol)
   operand-1
   operand-2) =>  BINOP

Comme précédemment, cela définit une fonction constructeur make-binop et trois fonctions de sélection binop-operator, binop-operand-1 et binop-operand-2. Il définit également un binop-p prédicat. L'effet de make-binop est maintenant de construire une liste de longueur quatre:

(make-binop :operator '+ :operand-1 'x :operand-2 5) =>  (BINOP + X 5)
 (make-binop :operand-2 4 :operator '*) =>  (BINOP * NIL 4)
La structure a la même disposition que précédemment, sauf que le nom de la structure binop est inclus comme premier élément de liste. Les fonctions de sélection binop-operator, binop-operand-1 et binop-operand-2 sont essentiellement équivalentes à cadr, caddr et cadddr, respectivement. Le prédicat binop-p est plus ou moins équivalent à cette définition:

(defun binop-p (x)
   (and (consp x) (eq (car x) 'binop))) =>  BINOP-P
Le nom binop n'est toujours pas un spécificateur de type valide reconnaissable à typep, mais au moins il existe un moyen de distinguer les structures binop des autres structures définies de manière similaire.

:prédicate

 Cette option prend un argument, qui spécifie le nom du prédicat de type. Si l'argument n'est pas fourni ou si l'option elle-même n'est pas fournie, le nom du prédicat est créé en concaténant le nom de la structure à la chaîne "-P", en internant le nom dans le package courant au moment où defstruct est étendu. Si l'argument est fourni et est nul, aucun prédicat n'est défini. Un prédicat ne peut être défini que si la structure est nommée; si :type est fourni et :named n'est pas fourni, alors: le prédicat doit être non fourni ou avoir la valeur nil.

:print-function, :print-object

Les options :print-function et :print-object spécifient qu'une méthode print-object pour les structures de type structure-name doit être générée. Ces options ne sont pas synonymes, mais offrent un service similaire; le choix de l'option (:print-function ou :print-object) utilisée affecte la façon dont la fonction nommée printer-name est appelée. Une seule de ces options peut être utilisée et ces options ne peuvent être utilisées que si :type n'est pas fourni.

Si l'option :print-function est utilisée, alors lorsqu'une structure de type structure-name doit être imprimée, la fonction d'imprimante désignée est appelée avec trois arguments:

    • la structure à imprimer (une instance généralisée de structure-nom).
      
    • un flux à imprimer.
      
    • un entier indiquant la profondeur actuelle. L'amplitude de cet entier peut varier entre les implémentations; cependant, il peut être comparé de manière fiable à *print-level* pour déterminer si l'abréviation de profondeur est appropriée.

La spécification (:print-function printer-name) équivaut approximativement à la spécification:

 (defmethod print-object ((object structure-name) stream)
   (funcall (function printer-name) object stream <<current-print-depth>>))
où <<current-print-depth>> représente ce que l’imprimante est susceptible de prendre quant à la profondeur d'impression actuelle. Cela dépend de l'implémentation si <<current-print-depth>> est toujours 0 et *print-level*, si non nul, est lié à des valeurs successivement plus petites à mesure que l'impression descend récursivement, ou si <<current-print-depth>> varie en valeur à mesure que l'impression descend récursivement et que le *print-level* reste constant pendant tout ce temps.

Si l'option :print-object est utilisée, alors lorsqu'une structure de type structure-name doit être imprimée, la fonction d'imprimante désignée est appelée avec deux arguments:

    •      la structure à imprimer.
      
    •      le flux vers lequel imprimer.

Spécifier (:print-object printer-name) équivaut à spécifier:

 (defmethod print-object ((object structure-name) stream)
   (funcall (function printer-name) object stream))
Si aucune option :type n’est fournie, et si une option :print-function ou une option :print-object est fournie et si aucun nom d'imprimante n'est fourni, une méthode print-object spécialisée pour structure-name est générée qui appelle une fonction qui implémente le comportement d'impression par défaut pour les structures utilisant la notation #S; voir Section 22.1.3.12 (Structures d'impression).

Si ni une option :print-function ni une option :print-object n'est fournie, defstruct ne génère pas de méthode print-object spécialisée pour structure-name et un comportement par défaut est hérité soit d'une structure nommée dans une option :include, soit du comportement par défaut pour les structures d'impression; voir la fonction print-object et la Section 22.1.3.12 (Structures d'impression).

Lorsque *print-circle* est vrai, une fonction d'impression définie par l'utilisateur peut imprimer des objets dans le flux fourni en utilisant write, prin1, princ ou format et s'attendre à ce que les circularités soient détectées et imprimées à l'aide de la syntaxe #n#. Cela s'applique aux méthodes sur print-object en plus des options de fonction d'impression. Si une fonction d'impression définie par l'utilisateur imprime sur un flux autre que celui qui a été fourni, la détection de circularité recommence pour ce flux. Voir la variable *print-circle*.

:type

:type spécifie explicitement la représentation à utiliser pour la structure. Son argument doit être l'un de ces types:

vector

 Cela produit le même résultat que la spécification (vecteur t). La structure est représentée comme un vecteur général, stockant les composants comme des éléments vectoriels. Le premier composant est l'élément vectoriel 1 si la structure est :named, et l'élément 0 sinon.

(vector element-type)

 La structure est représentée comme un vecteur (éventuellement spécialisé), stockant les composants comme des éléments vectoriels. Chaque composant doit être d'un type pouvant être stocké dans un vecteur du type spécifié. Le premier composant est l'élément vectoriel 1 si la structure est :named, et l'élément 0 sinon. La structure peut être :named uniquement si le symbole de type est un sous-type du type d'élément fourni.

list
 La structure est représentée sous forme de liste. Le premier composant est le CADR si la structure est :named et le CAR si elle ne l'est pas.
 La spécification de cette option a pour effet de forcer une représentation spécifique et de forcer les composants à être stockés dans l'ordre spécifié dans defstruct dans les éléments successifs correspondants de la représentation spécifiée. Il empêche également le nom de la structure de devenir un spécificateur de type valide reconnaissable par typep.

Par exemple :

 (defstruct (quux (:type list) :named) x y)

devrait faire un constructeur qui construit une liste exactement comme celui que la liste produit, avec quux comme CAR.

Si ce type est défini:

 (deftype quux () '(satisfies quux-p))

alors ce formulaire

 (typep (make-quux) 'quux)
devrait retourner précisément ce que celui-ci fait

(typep (list 'quux nil nil) 'quux)
Si :type n'est pas fourni, la structure est représentée comme un objet de type structure-object.

 defstruct sans une option :type définit une classe avec le nom de la structure comme nom. La métaclasse des instances de structure est structure-class.

Les conséquences de la redéfinition d'une defstruct structure ne sont pas définies.

Dans le cas où aucune option de defstruct n'a été fournie, les fonctions suivantes sont automatiquement définies pour fonctionner sur les instances de la nouvelle structure:

Prédicat

Un prédicat portant le nom structure-name-p est défini pour tester l'appartenance au type de structure. Le prédicat (structure-name-p object) est vrai si un objet est de ce type; sinon c'est faux. typep peut également être utilisé avec le nom du nouveau type pour tester si un objet appartient au type. Un tel appel de fonction a la forme (typep object 'structure-name).

Fonctions du lecteur de composants

Les fonctions de lecture sont définies pour lire les composants de la structure. Pour chaque nom d'emplacement, il existe une fonction de lecture correspondante avec le nom structure-name-slot-name. Cette fonction lit le contenu de cet emplacement. Chaque fonction de lecture prend un argument, qui est une instance du type de structure. setf peut être utilisé avec n'importe laquelle de ces fonctions de lecteur pour modifier le contenu de l'emplacement.

Fonction constructeur

Une fonction constructeur portant le nom make-structure-name est définie. Cette fonction crée et renvoie de nouvelles instances du type de structure.

Fonction copieur

Une fonction de copieur avec le nom copy-structure-name est définie. La fonction copieur prend un objet du type de structure et crée un nouvel objet du même type qui est une copie du premier. La fonction copieur crée une nouvelle structure avec les mêmes entrées de composants que l'original. Les composants correspondants des deux instances de structure sont EQL.

 Si un formulaire defstruct apparaît en tant que formulaire de niveau supérieur, le compilateur doit faire en sorte que le nom du type de structure soit reconnu comme un nom de type valide dans les déclarations suivantes (comme pour le deftype) et faire connaître les lecteurs d'emplacement de structure à setf. En outre, le compilateur doit enregistrer suffisamment d'informations sur le type de structure pour que d'autres définitions de structure puissent utiliser :include dans un deftype ultérieur dans le même fichier pour faire référence au nom du type de structure. Les fonctions générées par defstruct ne sont pas définies dans l'environnement de compilation, bien que le compilateur puisse enregistrer suffisamment d'informations sur les fonctions pour coder les appels suivants en ligne. La macro de lecture #S peut ou non reconnaître le nom du type de structure nouvellement défini au moment de la compilation.

Exemples:

Voici un exemple de définition de structure:

(defstruct ship
   x-position
   y-position
   x-velocity
   y-velocity
   mass)
Cela déclare que chaque vaisseau est un objet avec cinq composants nommés. L'évaluation de ce formulaire fait ce qui suit:

    1.  Il définit la position x du navire comme une fonction d'un argument, un navire, qui renvoie la position x du navire; la position du navire et les autres composants reçoivent des définitions de fonctions similaires. Ces fonctions sont appelées fonctions d'accès, car elles sont utilisées pour accéder aux éléments de la structure.
       
    2.  ship devient le nom d'un type dont les instances de navires sont des éléments. le navire devient acceptable à taper, par exemple; (typep sh 'ship) est vrai si sh est un navire et faux si sh est un objet autre qu'un navire.
       
    3.  Une fonction nommée ship-p d'un argument est définie; c'est un prédicat qui est vrai si son argument est un navire et faux sinon.
       
    4.  Une fonction appelée make-ship est définie qui, lorsqu'elle est invoquée, crée une structure de données à cinq composants, utilisable avec les fonctions d'accès. Ainsi, en exécutant :
       
       (setq ship2 (make-ship))
       définit ship2 sur un objet navire nouvellement créé. On peut fournir les valeurs initiales de tout composant souhaité dans l'appel à faire en utilisant des arguments de mots clés de cette manière:
       
        (setq ship2 (make-ship :mass *default-ship-mass*
                               :x-position 0
                               :y-position 0))
       Cela construit un nouveau navire (une instance) et initialise trois de ses composants. Cette fonction (make-ship) est appelée « fonction constructeur » car elle construit une nouvelle structure.
       
    5.  Une fonction appelée copy-ship d'un argument est définie qui, lorsqu'elle reçoit un objet navire, crée un nouvel objet navire qui est une copie de l'objet donné. Cette fonction est appelée « fonction copieur ».

setf peut être utilisé pour modifier les composants d'un navire:

 (setf (ship-x-position ship2) 100)
Cela modifie la position x de ship2 à 100. Cela fonctionne parce que defstruct se comporte comme s'il générait un defsetf approprié pour chaque fonction d'accès.

;;;
;;; Example 1
;;; define town structure type
;;; area, watertowers, firetrucks, population, elevation are its components
;;;
 (defstruct town
             area
             watertowers
             (firetrucks 1 :type fixnum)    ;an initialized slot
             population 
             (elevation 5128 :read-only t)) ;a slot that can't be changed
=>  TOWN
;create a town instance
 (setq town1 (make-town :area 0 :watertowers 0)) =>  #S(TOWN...)
;town's predicate recognizes the new instance
 (town-p town1) =>  true
;new town's area is as specified by make-town
 (town-area town1) =>  0
;new town's elevation has initial value
 (town-elevation town1) =>  5128
;setf recognizes reader function
 (setf (town-population town1) 99) =>  99
 (town-population town1) =>  99
;copier function makes a copy of town1
 (setq town2 (copy-town town1)) =>  #S(TOWN...)
 (= (town-population town1) (town-population town2))  =>  true
;since elevation is a read-only slot, its value can be set only
;when the structure is created
 (setq town3 (make-town :area 0 :watertowers 3 :elevation 1200))
=>  #S(TOWN...)
;;;
;;; Example 2
;;; define clown structure type
;;; this structure uses a nonstandard prefix
;;;
 (defstruct (clown (:conc-name bozo-))
             (nose-color 'red)         
             frizzy-hair-p polkadots) =>  CLOWN
 (setq funny-clown (make-clown)) =>  #S(CLOWN)
;use non-default reader name
 (bozo-nose-color funny-clown) =>  RED        
 (defstruct (klown (:constructor make-up-klown) ;similar def using other
             (:copier clone-klown)              ;customizing keywords
             (:predicate is-a-bozo-p))
             nose-color frizzy-hair-p polkadots) =>  klown
;custom constructor now exists
 (fboundp 'make-up-klown) =>  true
;;;
;;; Example 3
;;; define a vehicle structure type
;;; then define a truck structure type that includes 
;;; the vehicle structure
;;;
 (defstruct vehicle name year (diesel t :read-only t)) =>  VEHICLE
 (defstruct (truck (:include vehicle (year 79)))
             load-limit                          
             (axles 6)) =>  TRUCK
 (setq x (make-truck :name 'mac :diesel t :load-limit 17))
=>  #S(TRUCK...)
;vehicle readers work on trucks
 (vehicle-name x)
=>  MAC
;default taken from :include clause 
 (vehicle-year x)
=>  79 
 (defstruct (pickup (:include truck))     ;pickup type includes truck
             camper long-bed four-wheel-drive) =>  PICKUP
 (setq x (make-pickup :name 'king :long-bed t)) =>  #S(PICKUP...)
;:include default inherited
 (pickup-year x) =>  79
;;;
;;; Example 4
;;; use of BOA constructors
;;;
 (defstruct (dfs-boa                      ;BOA constructors
               (:constructor make-dfs-boa (a b c)) 
               (:constructor create-dfs-boa
                 (a &optional b (c 'cc) &rest d &aux e (f 'ff))))
             a b c d e f) =>  DFS-BOA
;a, b, and c set by position, and the rest are uninitialized
 (setq x (make-dfs-boa 1 2 3)) =>  #(DFS-BOA...)
 (dfs-boa-a x) =>  1
;a and b set, c and f defaulted
 (setq x (create-dfs-boa 1 2)) =>  #(DFS-BOA...)
 (dfs-boa-b x) =>  2
 (eq (dfs-boa-c x) 'cc) =>  true
;a, b, and c set, and the rest are collected into d
 (setq x (create-dfs-boa 1 2 3 4 5 6)) =>  #(DFS-BOA...)
 (dfs-boa-d x) =>  (4 5 6)
Situations exceptionnelles:

Si deux noms d'emplacement (qu'ils soient présents directement ou hérités par l'option :include) sont identiques sous string=, defstruct doit signaler une erreur de type erreur de programme.

Les conséquences ne sont pas définies si le nom de structure inclus ne nomme pas de type de structure.

Fin de DEFSTRUCT de l'HyperSpec.
__________________________________


Voyons quelques exemples de structures:

Pour définir un point par ses coordonnées dans un plan (Dans l'espace vous ajoutez un slot: x, y, z.):
(on suppose qu'on travaille dans un repère orthonormé: d'ailleurs, je vous conseille de construire sur papier ce repère pour visualiser ce qu'on y définit)

(defstruct (point (:conc-name nil))
   x y)
(:conc-name nil) fait en sorte que la structure point n'a pas de nom (par défaut ce serait: "point").
Ainsi pour avoir l'abscisse d'un point P, on écrira (x p) au lieu de (point-x p): c'est plus pratique.
Pour définir le point origine O:

(defparameter O (make-point :x 0 :y 0))
#S(POINT :X 0 :Y 0)
au passage rappelons qu'il ne faut pas confondre O (la lettre) et 0 (le zéro).

(values (x O) (y O))
0
0 rend les coordonnées du point o.

On définit deux autres points A et B:

(defparameter A (make-point :x 1 :y 1))
(defparameter B (make-point :x 2 :y 3))

(defun vect (p1 p2)
   "Rend un vecteur d'origine p1 et d'extrémité p2."
   (coerce (mapcar #'- (list (x p2) (y p2)) (list (x p1) (y p1))) 'vector))
VECT

(vect A B)
#(1 2) Rend les coordonnées du vecteur AB

(defun produit-scalaire (v1 v2)
   (reduce #'+ (map 'vector #'* v1 v2)))
PRODUIT-SCALAIRE

(defun carré-scalaire (v)
   (produit-scalaire v v))
CARRÉ-SCALAIRE

(defun module (v)
   (sqrt (carré-scalaire v)))
MODULE

(module (vect A B))
2.236068

Pour définir des droites, voici une nouvelle structure:

(defstruct (droite)
   origine vecteur)
ici, on n'a pas mis de :conc-name.
(defparameter v (vect A B))
V

La droite d passant par O et de vecteur directeur v peut se définir ainsi:

(defparameter d (make-droite :origine O :vecteur v))
D

Est-ce bien une droite?

(droite-p d)
T
oui.
Pour avoir le vecteur unitaire correspondant à un vecteur v:

(defun vecteur-unitaire (v)
   "rend le vecteur unitaire colinéaire à l'argument."
   (let ((longueur (module v)) (l (coerce v 'list)))
     (coerce (mapcar #'(lambda (x) (/ x longueur)) l) 'vector)))
VECTEUR-UNITAIRE

(vecteur-unitaire v)
#(0.4472136 0.8944272)

Je veux savoir si le point B(2,3) se trouve sur la droite d passant par O et de vecteur directeur v:
Je cherche le vecteur unitaire du vecteur OB.

(vecteur-unitaire (vect O B))
#(0.5547002 0.8320503)
Ce ne sont pas les coordonnées du vecteur unitaire du vecteur v.
Donc B n'est pas sur la droite d.

Qu'en est-il du point C(2,4)?
On définit, d'abord le point C:

(defparameter C (make-point :x 2 :y 4))
C

(vecteur-unitaire (vect O C))
#(0.4472136 0.8944272)
cette fois les vecteurs unitaires de v et OC sont les mêmes.
C est donc sur la droite d.
On peut aussi le voir de cette façon:

(equalp (vecteur-unitaire v) (vecteur-unitaire (vect O C)))
T

Définissons la droite d1 passant par A et de vecteur directeur v:

(defparameter d1 (make-droite :origine A :vecteur v))
D1

Les droites d et d1 ont le même vecteur directeur: elles sont donc parallèles.
Voici une fonction qui vérifie si deux droites sont parallèles:

(defun parallèle-p (droite1 droite2)
   (if (and (droite-p droite1) (droite-p droite2))
       (equalp (vecteur-unitaire (droite-vecteur droite1)) (vecteur-unitaire (droite-vecteur droite2)))
       (error "Au moins un argument n'est pas une droite.")))
PARALLÈLE-P

Vérifions pour d et d1:

(parallèle-p d d1)
T
d et d1 sont bien parallèles.

(parallèle-p d A)
; Evaluation aborted on #<SIMPLE-ERROR "Au moins un argument n'est pas une droite." {10046F9333}>.
             En effet A est un point et non une droite.
     Si vous mettez un symbole non défini à la place de A, vous obtenez un message d'erreur signalant que ce symbole est unbound.

On définit un autre point E(2,1):

(defparameter E (make-point :x 2 :y 1))
E

Puis on définit la droite d2 passant par E et de vecteur directeur OE:

(defparameter d2 (make-droite :origine E :vecteur (vect O E)))
D2

(parallèle-p d d2)
NIL
les droites d et d2 ne sont pas parallèles.

Voici une autre fonction qui vérifie si deux droites sont perpendiculaires:

(defun perpendiculaire-p (droite1 droite2)
   (if (and (droite-p droite1) (droite-p droite2))
       (zerop (produit-scalaire (droite-vecteur droite1) (droite-vecteur droite2)))
       (error "Au moins un argument n'est pas une droite.")))
PERPENDICULAIRE-P

(perpendiculaire-p d d2)
NIL
les droites d et d2 ne sont pas perpendiculaires.

On définit un autre point F(-2,1):

(defparameter F (make-point :x -2 :y 1))
F

Puis on définit la droite d3 passant par F et de vecteur directeur OF:

(defparameter d3 (make-droite :origine F :vecteur (vect O F)))
D3

(perpendiculaire-p d d3)
T
d est perpendiculaire à d3.

Revenons à notre droite (d1) (celle qui passe par les points A et B.).
Son équation s'écrit y=ax+b où a est son coefficient directeur et b son ordonnée à l'origine.
Le coefficient directeur peut se calculer avec les coordonnées des points A et B:
a=(yB-yA)/(xB-xA) (c'est aussi le rapport des coordonnées du vecteur directeur v).
Les coordonnées du point d'intersection de la droite (d1) avec l'axe des ordonnées sont (0,b).
Donc a=(yA-b)/(xA-0). On en déduit que b=yA-a.xA.

(defun équ-drte (p1 p2)
           "Rend l'équation de la droite passant par les 2 points donnés en argument."
           (if (and (point-p P1) (point-p P2))
               (let* ((a (/ (- (y P2) (y P1)) (- (x P2) (x P1)))) (b (- (y P1) (* a (x P1)))))
                 (format t "L'équation de cette doite est y=~ax~@f ." (float a) (float b)))
               (error "Les deux arguments doivent être une structure point.")))

Vous avez remarqué ce ~@f dans format: c'est une directive propre à format (ce n'est pas du lisp).
Cela permet d'afficher le signe + quand b est positif (voir format: plus loin).

(équ-drte A B)
L'équation de cette doite est y=2x-1 .
NIL

(équ-drte F C)
L'équation de cette doite est y=0.75x+2.5 .
NIL

Encore une structure: celle d'un cercle défini par son centre et son rayon.

(defstruct (cercle)
   centre rayon)
CERCLE

définissons le cercle de centre A et de rayon AB (module de v):

(defparameter C1 (make-cercle :centre A :rayon (module v)))
C1

Construisons quelques fonctions concernant le cercle:

(defun circonf (cercle)
   (if (cercle-p cercle)
       (* 2 pi (cercle-rayon cercle))
       (error "Ce n'est pas un cercle.")))
CIRCONF

(circonf C1)
14.049629668361005d0

(defun aire (cercle)
   (if (cercle-p cercle)
       (* pi (expt (cercle-rayon cercle) 2))
       (error "Ce n'est pas un cercle.")))
AIRE

(aire C1)
15.707963267948966d0

(defun milieu (P1 P2)
           "Rend la liste des coordonnées du milieu du segment d'extrémités P1 et P2."
           (if (and (point-p P1) (point-p P2))
               (mapcar #'(lambda (x) (/ x 2)) (mapcar #'+ (list (x p1) (y p1)) (list (x p2) (y p2))))
               (error "Les deux arguments doivent être une structure point.")))
MILIEU

(milieu A B)
(3/2 2)

Pour définir le point I comme étant le milieu du segment [AB]:

(multiple-value-bind (x y) (values-list (milieu A B)) (defparameter I (make-point :x x :y y)))
I

Vérifions que I a bien une structure point et que ses coordonnées sont bien celles précédemment calculées:

I
#S(POINT :X 3/2 :Y 2)

Il peut être intéressant de définir une fonction pour nommer un point défini par ses coordonnées:

(defun nomme (LST pt)
           "le 1er argument est la liste (quotée) des coordonnées du point et le 2ème argument est le nom (quoté) attribué au point."
           (defparameter var1 ())
           (setq var1 pt)
           (multiple-value-bind (x y) (values-list LST) (set var1 (make-point :x x :y y))))

Voici une fonction qui permet de choisir la dimension de l'espace sur lequel on travaille.
Mais ce n'est là qu'une idée à développer si vous voulez faire une application conséquente.

(defun coord (x &optional (y nil) (z nil) (s nil))
           "Indique la dimension de l'espace suivant le nombre (entre 1 et 4) de coordonnées fournies à la fonction coord."
           (cond ((null (cadr (list x y z s))) (format t "C'est un espace à une seule dimension."))
                 ((null (caddr (list x y z s))) (format t "C'est un espace à deux dimensions."))
                 ((null (cadddr (list x y z s))) (format t "C'est un espace à trois dimensions."))
                 (t (format t "C'est un espace de Minkowski"))))

(defun translation (v pt)
           "Rend les coordonnées du point-image du point fourni en deuxième argument par la translation de vecteur fourni en premier argument."
           (if (vectorp v)
               (if (point-p pt)
                   (mapcar #'+ (list (x pt) (y pt)) (coerce v 'list))
                   (error "Le 2ème argument doit être une structure point."))
               (error "Le 1er argument doit être de type vector.")))

Cherchons les coordonnées du point D de sorte que ABCD soit un parallélogramme (A, B et Csont les points définis précédemment.):

(translation (vect B C) A)
(1 2)

ou bien:

(translation (vect B A) C)
(1 2)

Si on veut nommer le point D directement, on peut utiliser la fonction nomme avec la fonction translation:

(nomme (translation (vect B C) A) 'D)
#S(POINT :X 1 :Y 2)

D
#S(POINT :X 1 :Y 2)

La prochaine fois: CLASSE D'OBJET et initialisation.