next up previous
Previous: No Title

Sous-sections

Les objets d'O'Caml : partie II




Objectifs :Présentation du noyau objet du langage O'CAML : héritage multiple, classe paramétréees, contraintes de typage

Objet et type

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 = 2
En 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
`u les 2 points indiquent la possibilité de l'augmenter par le type de nouvelles méthodes.

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.

Héritage multiple

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_rectangle
Dans 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
                \          /
                 \        /
                  \      /
                  rectangle
Si 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.

Classes paramétrées

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.

exemple

Par exemple si l'on désire construire une classe pair pour décrire les paires, une solution nai"ve serait :
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 unbound
Cette 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 = ()

contraintes de typage

Selon l'usage de la valeur du type paramétré, des contraintes de typage peuvent apparaître dans l'interface inférée. On cherche à construire des listes paramétrées sans utiliser de vecteurs. Pour cela on définit une classe abstraite ['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
  end
puis 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 = ()

contrainte explicite

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;;

Autres lectures

Ce cours s'inspire principalement des mêmes documents que le cours précédent :


next up previous
Previous: No Title
Emmanuel CHAILLOUX
1998-11-15