Classe d'objet.
CLASS-OF: pour trouver la classe d'un objet.
(class-of 'a)
#<BUILT-IN-CLASS SYMBOL>
(class-of "a")
#<BUILT-IN-CLASS SB-KERNEL::SIMPLE-CHARACTER-STRING>
"a" est un objet de la classe SB-KERNEL::SIMPLE-CHARACTER-STRING
(class-of '(a b))
#<BUILT-IN-CLASS 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.
____________________
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.
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
#
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)))))
#
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))
#
(defmethod solde1 ((compte compte-bancaire)) ;la méthode est liée à la classe compte-bancaire
(slot-value compte 'solde))
#
(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)))))
#
(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.