On se propose de réaliser en Scheme un système de programmation orientée objet (poo).
Un objet est une entité capable de répondre à des messages, en fonction de son état propre matérialisé par la valeur de ses "champs" (en anglais "slot" au singulier). Par exemple un bateau ("ship" en anglais) navigant à la vitesse de 30 noeuds répondra au message "accélérer de 10 noeuds" en portant sa vitesse à 40 noeuds.
Des objets pourvus des mêmes champs et des mêmes méthodes de réaction à des messages sont dits appartenir à une même classe, par exemple, la classe "ship". On dit que ces objets sont des instances de la classe.
Une des richesse de la programmation orientée objet est le concept d'héritage, qui permet à une classe d'hériter de champs ou méthodes d'une autre classe (plus générale) appelée "superclasse". Par exemple, un "destroyer" est une sorte de "ship" possédant en plus des canons et des missiles (sous forme de champs supplémentaires).
Il existe différentes manières de concevoir et d'implanter un système d'objets en LISP. Common LISP incorpore un système d'objets très riche, extrêmement efficace en temps et en espace, mais compliqué à implanter.
Le système que l'on se propose de construire en Scheme est bien plus réduit, quoique présentant les concepts essentiels de la poo. Il n'est pas forcément très efficace en espace, mais il est relativement simple à implanter, grâce à l'utilisation des fermetures pour représenter les objets.
Une fermeture est une fonction munie d'un environnement local susceptible
d'évoluer avec le temps. Un objet sera représenté
par une fermeture contenant les champs de l'objet (son environnement local)
et encapsulant également toutes les méthodes propres à
la classe de l'objet.. L'encapsulation lexicale des méthodes dans
l'environnement local de l'objet leur donne un accès immédiat,
en lecture ou en écriture, aux champs de l'objet; une contrepartie
est que le code des méthodes est dupliqué dans toutes les
instances d'une même classe, au lieu d'être partagé,
d'où la consommation en espace. Dans cette conception, l'envoi d'un
message à un objet revient à appliquer l'objet (une fonction)
au message (un symbole) suivi de ses arguments spécifiques.
(define ship1Cn notera que la méthode "stop" n'utilise pas le paramètre "arguments", alors que "accelerate" utilise le premier élément de "arguments", et ignore les autres, mais la référence "(car arguments)" n'est pas très parlante: on préfèrerait appeler "delta" cet argument qui représente un incrément pour la vitesse. Néanmoins, on peut déjà utiliser cet objet, e.g.:
(let ((speed 30) ; environnement local des champs
(captain 'Nemo)) ; de l'objet "ship1".
(lambda (message . arguments) ; l'objet "ship1" lui-même.
(case message ; décodage du message.
('stop (set! speed 0))
('accelerate (set! speed (+ (car arguments) speed)))
(else (error "Undefined method!"))))))
(ship1 'accelerate 10) ; accélérer de 10 noeuds!mais on n'a aucun moyen de contrôler que la vitesse de ship1 est bien passée à 40 noeuds.
Voici un second bateau fait main mais plus perfectionné, pour remédier à ces deux défauts:
(define ship2On peut en effet consulter la vitesse et le capitaine de l'extérieur de l'objet, car ce sont des messages définis par des méthodes:
(let ((speed 20) ; environnement local des champs
(captain 'Haddock)) ; de l'objet "ship2".
;;
;; Environnement de méthodes:
(let ((stop (lambda () (set! speed 0)))
(accelerate (lambda (delta) (set! speed (+ delta speed)))))
(lambda (message . arguments) ; l'objet "ship2" lui-même.
(case message
('speed speed)
('captain captain)
('stop (apply stop arguments))
('accelerate (apply accelerate arguments))
(else (error "Undefined method!")))))))
;;; (ship2 'speed)Ces exemples sont traités dans le fichier hand_made_objects.scm que l'on pourra tester sous Scheme.
;;; (ship2 'captain)
;;; (ship2 'accelerate 10)
;;; (ship2 'speed)
Faire des objets à la main s'avère peu pratique, car il faut répéter le code des méthodes pour chaque instance, et l'on voudrait disposer d'un constructeur qui nous affranchisse de ce travail.
;;;Notons que le "letrec" établit un environnement fonctionnel qui définit non seulement les deux méthodes "stop" et "accelerate", mais aussi la fonction "self" représentant l'objet lui-même (le décodeur du message): ceci permet à chaque méthode traitant l'envoi d'un message d'envoyer un (autre) message à l'objet lui-même, ce qui est ici le cas pour la méthode "accelerate". Les méthodes "speed" et "captain" ne sont pas déclarées dans le "letrec", pour ne pas cacher les slots "speed" et "captain": ces deux méthodes permettent de consulter ou modifier les champs correspondants:
;;; Un constructeur de bateaux ordinaires:
;;;
(define (make_ship . init_plist)
(let ((speed (or (get 'speed init_plist) 30))
(captain (or (get 'captain init_plist) 'Nemo)))
(letrec ((stop (lambda () (set! speed 0)))
(accelerate (lambda (delta) (self 'speed (+ delta speed))))
(self
(lambda (message . arguments)
(case message
('speed (if (null? arguments)
speed
(set! speed (car arguments))))
('captain (if (null? arguments)
captain
(set! captain (car arguments))))
('stop (apply stop arguments))
('accelerate (apply accelerate arguments))
(else (error (string-append "Method "
(symbol->string message)
" for class ship is undefined!")))))))
self)))
;;; (define ship3 (make_ship 'speed 99)) ; fabrication d'un bateau,Voici maintenant un constructeur de "destroyer": la classe "destroyer" hérite de la classe "ship" en ajoutant les champs "missiles" et "guns" , la méthode "decelerate", et en adaptant la méthode "accelerate":
;;; (ship3 'speed) ; consultation de la vitesse,
;;; (ship3 'captain);;; (ship3 'speed 112) ; modification de la vitesse.
;;; (ship3 'speed);;; (ship3 'accelerate 11)
;;; (ship3 'speed)
;;;L'héritage se manifeste dans le constructeur par la présence du champ "super" dont la valeur est initialisée à une instance de "ship". Le décodeur de messages délègue d'ailleurs tout message incompris à cette instance (clause "else" du "case"). Ce champ "super" permettre en outre à la classe "destroyer" d'adapter la méthode "accelerate" héritée de la classe "ship": l'incrément "delta" est multiplié par 2.
;;; Un destroyer est une sorte de bateau:
;;;
(define (make_destroyer . init_plist)
(let ((super (apply make_ship init_plist))
(missiles (or (get 'missiles init_plist) *no_default*))
(guns (or (get 'guns init_plist) 105)))
(letrec ((accelerate (lambda (delta)
(super 'accelerate (+ delta delta))))
(decelerate (lambda (delta)
(self 'accelerate (- delta))))
(self
(lambda (message . arguments)
(case message
('missiles (if (null? arguments)
missiles
(set! missiles (car arguments))))
('guns (if (null? arguments)
guns
(set! guns (car arguments))))
('accelerate (apply accelerate arguments))
('decelerate (apply decelerate arguments))
(else (apply super message arguments))))))
self)))
On pourra tester bateaux et destroyers en chargeant le fichier ship_factory.scm
dans l'environnement de Scheme.
Les deux "defclass" qui suivent auront ainsi pour effet de définir les constructeurs "make_ship" et "make_destroyer" précédents:
(defclass ship ; ship class has no superclass.Le squelette de la macro "defclass" a la forme suivante:
(slots (speed 30)
(captain 'Nemo))
(methods (defmethod (stop) (set! speed 0))
(defmethod (accelerate delta)
(self 'speed (+ delta speed)))))(defclass (destroyer ship) ; destroyer is a kind of ship.
(slots missiles ; no default provided.
(guns 105))
(methods (defmethod (accelerate delta)
(super 'accelerate (+ delta delta)))
(defmethod (decelerate delta)
(self 'accelerate (- delta)))))
(define-macro (defclass class_or_class_super slot_spec method_spec)avec les paramètres suivants:
???)
guile> (ship 'name)Technique: La construction "block" de Scheme peut être utilisée pour définir une macro engendrant une expansion contenant plusieurs formes LISP, devant chacune un effet sur l'environnement global. Par exemple:
shipguile> (ship 'superclass)
#fguile> (ship 'slots)
((speed 30) (captain 'Nemo))guile> (ship 'methods)
((defmethod (accelerate delta)
(super 'accelerate (+ delta delta)))
(defmethod (decelerate delta)
(self 'accelerate (- delta)))))guile> (destroyer 'superclass)
shipguile> (destroyer 'slots)
(missiles (guns 105))guile> (class 'slots)
(name (superclass ()) slots methods)guile> (class 'methods)
((defmethod (make . init_plist) ???)
...)
guile> (block (define (make_ship . init_plist) ...)a le même effet sur l'environnement global que
(define ship ...))
guile> (define (make_ship . init_plist) ...)Raffinement: Etant donné un objet quelconque, comment peut-on retrouver sa classe? Pour permettre à tout objet de répondre par son nom de classe au message "class", on peut créer une classe standard de nom "standard_class" dont hérite toute autre classe, directement ou indirectement. Ainsi la classe "ship" hériterait par défaut de cette classe standard, munie de la méthode "class".
guile> (define ship ...)