Objectifs :Présentation du noyau objet du langage O'CAML : héritage multiple, classe paramétréees, contraintes de typage
Le type d'un objet est le type de ses méthodes. Par exemple le type point est une abréviation du type :
point = < distance : unit -> float; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit >C'est un type objet fermé, c'est à dire que le type de toutes ses méthodes est déterminé. Et il n'y en a pas d'autres.
Lors d'un envoi de message l'inférence de types construit un type objet ouvert :
# let f x = x#get_x;; val f : < get_x : 'a; .. > -> 'a = <fun> # let p = new point(2,3);; val p : point = <obj> # f p;; - : int = 2En effet f peut s'appliquer à n'importe quelle instance de classe ayant une méthode get_x qui retourne une valeur d'un type quelconque. Un type objet ouvert est représenté par la notation
< ..>
, o
Si l'on veut passer de la représentation d'un type objet fermé en un type objet ouvert, on utilisera alors la notation #type_obj
comme dans l'exemple suivant :
# let g (x : #point) = x#amess;; val g : < amess : 'a; distance : unit -> float; get_x : int; get_y : int; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit; .. > -> 'a = <fun>
où la coercion de type avec #point
force x à avoir au moins toutes les méthodes de point, et l'envoi du message amess ajoute une méthode au type du paramètre x.
L'héritage multiple permet d'hériter des champs de données et des méthodes de plusieurs classes. En cas de noms de champs ou de méthodes identiques, seulement la dernière déclaration, dans l'ordre de la déclaration de l'héritage, sera conservée. Les différentes classes héritées n'ont pas forcément de liens d'héritage entre elles.
L'intérêt de l'héritage multiple est d'augmenter la réutilisabilité des classes.
On définit la classe abstraite geometric_object qui déclare deux méthodes compute_area et compute_circ pour le calcul de la surface et du périmètre.
class virtual geometric_object () = object method virtual compute_area : unit -> float method virtual compute_circ : unit -> float end;;
On redéfinit la classe rectangle de la manière suivante :
class rectangle (p1,p2) = object inherit graphical_object () inherit geometric_object () val mutable llc = (p1 : point) val mutable ruc = (p2 : point) method print () = begin print_string "(";p1#print(); print_string ",";p2#print(); print_string ")" method compute_area() = abs(ruc#get_x - llc#get_x) * abs(ruc#get_t - llc#get_y) method compute_circ() = (abs(ruc#get_x - llc#get_x) + abs(ruc#get_t - llc#get_y)) * 2 end ;;
On obtient le graphe d'héritage suivant :
graphical_object geometric_object \ / \ / \ / rectangle
On aurait pu aussi ne pas réécrire la classe rectangle, mais dériver de rectangle de la manière suivante :
graphical_object \ \ \ rectangle geometric_object \ / \ / \ / n_rectangleDans ce cas là, seules les méthodes abstraites de la classe abstraite geometric_object auraient du être définies dans n_rectangle.
Toujours dans la même veine, les développement de la hiérarchie graphical_object et geometric_object auraient pu être séparés jusqu'au moment il devenait utile d'avoir une classe possèdant les deux comportements :
graphical_object geometric_object | | | | | | rectangle_graph rectangle_geo \ / \ / \ / rectangleSi on suppose que chaque classe rectanglai déclare des variables d'instances pour les coins d'un rectangle, on se retrouve dans la classe rectangle avec 4 points (2 par coin).
class rectabgle (p1,p2) = inherit rectangle_graph (p1,p2) as super_graph inherit rectangle_geo (p1,p2) as super_geo end;;
Dans le cas où des méthodes de même type existent dans les 2 classes rectanglei, alors seule la dernière est visible. Néanmoins, grace au nommage des class ancêtres (super..., il est toujours possible d'invoquer
une méthode d'une certaine hiérarchie.
Les classes paramétrées permettent d'utiliser le polymorphisme paramétrique de Caml dans les classes. Une déclaration de classe peut être paramétrée par des variables de types, comme dans une déclaration de types de Caml. Cela offre de nouvelles possibilités de généricité, pour la réutilisation du code, et s'intègre dans le typage à la ML quand l'inférence de type produit des types paramétrés.
class pair a b = object val x = a val y = b method fst = x method snd = y end;;et produirait l'erreur de typage suivante :
# class pair a b = object val x = a val y = b method fst = x method snd = y y end;; Some type variables are unbound in this type: class pair : 'a -> 'b -> object val x : 'a val y : 'b method fst : 'a method snd : 'b end The method fst has type 'a where 'a is unboundCette erreur de typage indique que la variable de type 'a du type de a, x et fst n'est pas liée. Pour que le typage reste correct, il est nécessaire de paramétriser la classe pair de la manière suivante :
class ['a,'b] pair (a:'a) (b:'b) = object val x = a val y = b method fst = x method snd = y end;;qui indique une interface de classe paramétrée par les variables de type 'a et 'b :
class ['a,'b] pair (a:'a) (b:'b) = object val x = a val y = b method fst = x method snd = y end;; class ['a, 'b] pair : 'a -> 'b -> object val x : 'a val y : 'b method fst : 'a method snd : 'b end
En reprenant l'exemple sur les piles, il est donc possible de construire une classe paramétrée sur les piles :
class ['a] pile ((x:'a),n) = object(self) val mutable ind = 0 val tab = Array.create n x method is_empty () = if ind = 0 then true else false method private is_full () = if ind = n+1 then true else false method pop() = if self#is_empty() then failwith "pile vide" else ind <- ind -1 ; tab.(ind) method push y = if self#is_full() then failwith "pile pleine" else tab.(ind) <- y; ind <- ind + 1 end;;qui produit l'interface suivante :
class ['a] pile : 'a * int -> object val mutable ind : int val tab : 'a array method is_empty : unit -> bool method private is_full : unit -> bool method pop : unit -> 'a method push : 'a -> unit end
On peut alors créer et manipuler des piles paramétrées de la manière suivante :
# let pi = new pile (0.0,10);; val pi : float pile = <obj> # pi#push(3.14);; - : unit = () # let ps = new pile ("hello", 20);; val ps : string pile = <obj> # ps#push("hello");; - : unit = () # let pp = new pile (new point(0,0),10);; val pp : point pile = <obj> # pp#push(new point(4,5));; - : unit = ()
['a] liste
ainsi que deux sous-classes :
['a] cons
et ['a] nil
de la manière suivante :class virtual ['a] liste () = object method virtual empty : unit -> bool method virtual cons : 'a -> unit method virtual head : 'a method virtual tail : 'a liste method virtual display : unit -> unit end;;dont l'interface est
class virtual ['a] liste : unit -> object method virtual cons : 'a -> unit method virtual display : unit -> unit method virtual empty : unit -> bool method virtual head : 'a method virtual tail : 'a liste endpuis la classe cons :
class ['a] cons (v,l) = object (self) inherit ['a] liste () val mutable car = (v:'a) val mutable cdr = (l:'a liste) method empty () = false method cons x = cdr<-new cons(car,cdr); car<-x method head = car method tail = cdr method display () = begin car#print(); print_string " ::"; self#tail#display() end end;;dont l'interface tiendra compte de la contrainte de types sur le paramètre de type 'a, contrainte due à l'envoi de la méthode print sur la variable d'instance car de type 'a.
class ['a] cons : 'a * 'a liste -> object constraint 'a = < print : unit -> 'b; .. > val mutable car : 'a val mutable cdr : 'a liste method cons : 'a -> unit method display : unit -> unit method empty : unit -> bool method head : 'a method tail : 'a liste end
Où une contrainte de type est posée sur la variable de type 'a qui indique que le type 'a doit pouvoir s'unifier avec le type de la contrainte. Cette contrainte provient de la méthode print envoyée sur le car dans la méthode display.
La classe nil permet juste de construire une fin de liste, utilisée quand crée une instance de cons. Pour cela son implantation de la méthode cons. Elle n'a pas de contrainte sur le paramètre de type.
exception ListeVide;; class ['a] nil () = object (self) inherit ['a] liste () val nil = () method empty () = true method cons (x:'a) = failwith "bad argument" method head = raise ListeVide method tail = raise ListeVide method display () = print_string "[]" end;;avec l'interface inférée suivante :
class ['a] nil : unit -> object val nil : unit method cons : 'a -> unit method display : unit -> unit method empty : unit -> bool method head : 'a method tail : 'a liste end
On définit par ailleurs une classe integer possèdant une méthode print. On pourra ainsi construire une liste d'integer.
class integer i = object val v = i method get = v method print () = print_int v end;;
La construction de liste est la suivante :
# let i1 = new integer 1;; val i1 : integer = <obj> # let i2 = new integer 2;; val i2 : integer = <obj> # let n = new nil ();; val n : '_a nil = <obj> # let l = new cons (i1,n);; val l : integer liste = <obj> # l#display();; 1 ::[]- : unit = ()
Cette contrainte sur les variables de type des classes paramétrées peut être explicite.
class virtual printable () = object method virtual print : unit -> unit end;; class ['a] cons (v,l) = object (self) inherit ['a] liste () constraint 'a = #printable val mutable car = (v:'a) val mutable cdr = (l:'a liste) method empty () = false method cons x = cdr<-new cons(car,cdr); car<-x method head = car method tail = cdr method display () = begin car#print(); print_string " ::"; self#tail#display() end end;;
Ce cours s'inspire principalement des mêmes documents que le cours précédent :
et de Roberto Di Cosmo au Magistère MMFAI .