Objectifs :Présentation du noyau objet du langage O'CAML : relation de sous-typage, style fonctionnel, interfaces et modules, simulation d'héritage avec les modules
Le sous-typage est la possibilité pour un objet d'un certain type d'être considéré et utilisé comme un objet d'au autre type.
point_colore :> pointSi le membre gauche de la relation est omis, alors c'est le type de la valeur qui sera considéré comme membre gauche.
Soient les déclarations suivantes
# let p = new point (4,5);; val p : point = <obj> # let pc = new point_colore (4,5) "blanc";; val pc : point_colore = <obj> # let np = (pc :> point);; val np : point = <obj> # let np2 = (pc : point_colore :> point);; val np2 : point = <obj>L'invocation de la méthode print sur les différents points donnera :
# p#print();; ( 4 , 5)- : unit = () # pc#print();; ( 4 , 5) de couleur blanc- : unit = () # np#print();; ( 4 , 5) de couleur blanc- : unit = ()où l'envoi d'un message print sur np, valeur considérée de type point déclenche la méthode print de la classe point_colore.
Cela permet par exemple de construire une liste de points contenant en fait des instances de point_colore :
# let l = [p;np];; val l : point list = [<obj>; <obj>] # List.map (fun x -> x#print()) l;; ( 4 , 5)( 4 , 5) de couleur blanc- : unit list = [(); ()]
Cela vient de la liaison tardive (choix de la méthode à utiliser à l'exécution). La combinaison de liaison tardive et sous-typage autorise une nouvelle forme de polymorphisme, le polymorphisme d'inclusion.
Deuxièmement il est aussi possible d'avoir un héritage de classes sans pouvoir faire du sous-typage entre instances de ces classes. On ajoute une méthode egalite à la classe point qui prend un argument de type de la classe en entrée. Dans la sous-classe point_colore on redéfinit la même méthode dont le type de l'argument est toujours le type de la classe en cours de définition. Si l'on essaie de faire du sous-typage entre deux instances de ces classes, alors il serait possible d'envoyer le message egalite à une instance de point_colore qui a été coercée en point avec un paramètre de type point. Dans ce cas la recherche d'un champs couleur dans l'argument de type point provoquerait une erreur à l'exécution.
class virtual egal () = object(self : 'a) method virtual eq : 'a -> bool end;; class point (x_init,y_init) = object(self) inherit egal () val mutable x = x_init val mutable y = y_init method eq q = (self#get_x = q#get_x ) && (self#get_y = q#get_y) method get_x = x method get_y = y method moveto (a,b) = begin x <- a; y <- b end method rmoveto (dx,dy) = begin x <- x + dx; y <- y + dy end method print () = begin print_string "( "; print_int x; print_string " , "; print_int y; print_string ")"; end method distance () = sqrt(float(x*x + y*y)) end;; class point_colore p c = object (self) inherit point p as super val c = c method eq q = (self#get_x = q#get_x ) && (self#get_y = q#get_y) && (self#get_c = q#get_c) method get_c = c method print () = begin super#print(); print_string (" de couleur "^ self#get_c); end end;;L'exécution du programme suivant entraîne un (long) message d'erreur :
# let p = new point (2,3);; val p : point = <obj> # let pc = new point_colore (2,3) "blanc";; val pc : point_colore = <obj> # let np = (pc :> point);; This expression cannot be coerced to type point = < distance : unit -> float; eq : point -> bool; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit >; it has type point_colore = < distance : unit -> float; eq : point_colore -> bool; get_c : string; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit > but is here used with type < distance : unit -> float; eq : point -> bool; get_c : string; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit > Type point_colore = < distance : unit -> float; eq : point_colore -> bool; get_c : string; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit > is not compatible with type point = < distance : unit -> float; eq : point -> bool; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit > Only the first object type has a method get_c
Cette partie est fortement inspirée du cours de Francois Barthélémy et maria-Virginia Aponte au CNAM.
Soient
et
où est une suite de méthodes,
on dit que est un sous-type de dans (contexte de typage), noté
si
pour
.
Si
,
et
alors est bien typé dans C et a le type .
Une fonction qui attend un argument de type peut recevoir sans danger un argument d'un sous-type de .
Si on définit les classes suivantes :
class a = ... method f : t1 -> t2 ... end;; class b = ... method f : t3 -> t4 ... end;;
Si on veut montrer que alors il faut vérifier . Pour distinguer les deux méthodes on les nomme : et .
Soient
et
deux types fonctionnels,
ils sont en relation de sous-typage :
si et seulement si :
justifications
Soient les 2 fonctions suivantes bien typées :
let g (p : t2) = ... let h ((o:a),(x:t1)) = g(o#f(x));;avec
g : t2 -> nt h : ( a * t1) -> nt
exemple
En reprenant l'exemple sur les point et point_colore précédent, on obtient :
et on s'aperçoit alors que pour que
il faudrait que
c'est à dire, avec la relation de contra-variance des types fonctionnels
ce qui est faux. Donc n'est pas un sous-type de !!!
Les contraintes de type sur les paramètres de types d'une classe paramétrées. Cela permet de restreindre le polymorphisme paramétrique par le polymorphisme d'inclusion. Dans le but de construire des listes de graphical_object il est possible de préciser la contrainte de types printable par graphical_object. Ainsi pour toutes les classes pouvant être sous-typées en graphical_object, leurs instances respectives pourront être éléments de telles listes.
Le style de la programmation objet est le plus souvent impératif. Un message est envoyé à un objet qui modifie physiquement son état interne (ses champs de données). Néanmoins il est aussi possible d'aborder la programmation objet par le style fonctionnel. L'envoi d'un message à un objet retourne un nouvel objet.
{< ... >}
qui retourne une copie de l'objet (self) dans lequel les valeurs certains champs de données sont changées.
class point (x_init,y_init) = object val x = x_init val y = y_init method moveto (a,b) = {<x=a; y=b>} method rmoveto (dx,dy) = {<x=x+dx;y=y+dy>} method affiche () = begin print_string "( "; print_int x; print_string " , "; print_int y; print_string ")"; end end;;
qui possède l'interface suivante :
class point : int * int -> object ('a) val x : int val y : int method affiche : unit -> unit method moveto : int * int -> 'a method rmoveto : int * int -> 'a end
Les méthodes moveto et rmoveto retourne un objet. On peut donc envoyer à leur résultat un message, comme dans l'exemple suivant :
# let p = new point (2,3);; val p : point = <obj> # (p#rmoveto(10,10))#affiche();; ( 12 , 13)- : unit = () # p#affiche();; ( 2 , 3)- : unit = ()
La fonctionOo.copy retourne une copie d'un objet. Son type est le suivant :
(< .. > as 'a) -> 'a
Le style fonctionnel peut utiliser les classes paramétrées avec la difficulté supplémentaire, due au sous-typage, de ne pas faire apparaître le type de self comme type résultat d'une méthode. C'est à dire, le type de self ne doit pas sortir de la classe.
On redéfinit la classe abstraite ['a] liste
de la manière suivante :
class virtual ['a] liste () = object method virtual empty : unit -> bool method virtual cons : 'a -> 'a liste method virtual head : 'a method virtual tail : 'a liste method virtual display : unit -> unit end;;où la méthode cons retourne une nouvelle liste.
La classe cons doit alors coercer le type de self dans la méthode cons.
class ['a] cons (v ,l) = object (self) inherit ['a] liste () constraint 'a = #printable val car = v val cdr = l method empty () = false method cons x = new cons (x, (self : 'a #liste :> 'a liste)) method head = car method tail = cdr method display () = begin car#print(); print_string " ::"; self#tail#display() end end;;
La classe nil utilisant le constructeur cons ne subit pas de modification sur les contraintes de types.
exception ListeVide;; class ['a] nil () = object (self) inherit ['a] liste () val nil = () method empty () = true method cons x = new cons (x, new nil()) method head = raise ListeVide method tail = raise ListeVide method display () = print_string "[]" end;;
L'interface interf_point est déclarée de la manière suivante :
class type interf_point = object method get_x : int method get_y : int method moveto : (int * int ) -> unit method rmoveto : (int * int ) -> unit method print : unit -> unit method distance : unit -> float end;;
class type interf_point = object method distance : unit -> float method get_x : int method get_y : int method moveto : int * int -> unit method print : unit -> unit method rmoveto : int * int -> unit end
Celle-ci peut dont être utilisée pour coercer le type d'une définition de classe.
# let f (x:interf_point) = x;; val f : interf_point -> interf_point = <fun>
Ces interfaces ne peuvent masquer que les champs de variables d'instance et les méthodes privées. Elles ne peuvent en aucun cas masquer des méthodes abstraites ou des méthodes publiques. C'est là que leur limitaion apparaît. Néanmoins, elles utilisent aussi la relation d'héritage (entre interfaces).
En fait l'intérêt principal de pouvoir construire des interfaces de classes sans avoir à définir la classe, provient de l'utilisation des modules. Ainsi il sera possible de construire la signature d'un module, utilisant des types objets, uniquement en donnant la description des interfaces de ces classes.
La programmation par modules et la programmation par objets ont des buts similaires : organisation logique d'un programme et compilation séparée. Elles permettent de faire de l'encapsulation de données, pour la réutilisation et la modification de composants, et d'étendre les fonctionnalités des composants. Il est à noter qu'avec les modules paramétrés il est possible de simuler les propriétés de l'héritage.
En effet soit le module Point de signature suivante :
module type POINT = sig type point val new_point : (int * int) -> point val get_x : point -> int val get_y : point -> int val moveto : point -> (int * int) -> unit val rmoveto : point -> (int * int) -> unit val affiche : point -> unit val distance : point -> float end;;
On peut construire un module Point_colore
de la manière suivante :
module Point_colore = functor (P : POINT) -> struct type point_colore = {p:P.point;c:string} let new_point_colore p c = {p=P.new_point p;c=c} let get_c self = self.c let get_x self = let super = self.p in P.get_x super let get_y self = let super = self.p in P.get_y super let moveto self = let super = self.p in P.moveto super let rmoveto self = let super = self.p in P.rmoveto super let distance self = let super = self.p in P.distance super let affiche self = let super = self.p in begin P.affiche super; print_string ("de couleur "^ self.c) end end;;La lourdeur des déclarations "héritées" peut être alégée par une procédure automatique de déclaration. Les déclarations récursives de méthodes pourraient s'écrirent par un seul
let rec ... and
. L'héritage multiple entraîne des
foncteurs à plusieurs paramêtres. Le coût de la redéfinition n'est pas plus
grand que la liaison tardive.
La principale différence entre programmation modulaire et programmation objet en O'Caml provient du système de types. En effet la programmation par modules reste dans le système de types à la ML, i.e. du polymorphisme paramétrique (même code exécuté pour différents types de paramètres), alors que la programmation par objets et la liaison tardive entraîne un polymorphisme ad hoc (où l'envoi d'un message à un objet déclenche l'application de codes différents). Cela est particulièrement clair avec le sous-typage. Cette extension du système de types à la ML ne peut se simuler en ML pur. Il sera toujours impossible de construire des listes hétérogènes sans casser le système de types.
En conclusion la programmation modulaire et la programmation par objets sont deux réponses sûres (grace au typage) à l'organisation logique d'un programme, permettent la réutilisabilité et la modifiabilité de composants logiciels. La programmation objet en O'Caml permet le polymorphisme paramétrique (classes paramétrées) et d'inclusion (envoi de messages) grace à la liaison tardive et au sous-typage, avec des restrictions dues à l'égalité, ce qui facilite une programmation incrémentale. La programmation modulaire reste dans le domaine du polymorphisme paramétrique sans restriction avec liaison immédiate ce qui peut être utile pour l'efficacité de l'exécution.
Ce cours s'inspire des mêmes documents que le cours précédent :
et de Roberto Di Cosmo au Magistere MMFAI .