TD/TP Caml n°5 : Les aspects objets en Objective Caml

1. Rappels et compléments de cours

Références :

Objective Caml propose une extension objet qui s'intégre au noyau fonctionnel et au noyau impératif, compatible avec le système de typage. L'association avec le polymorphisme confère au langage une puissance expressive importante. Il est à noter une limitation importante : Objective Caml n'intégre pas la notion de surcharge de méthodes (plusieurs définitions d'une même méthode) ... le système d'inférence de type risquerait sinon de rencontrer des ambiguités.

Définir une classe

Une classe définit à la fois un type et un moule pour les objets. Elle contient des données et des méthodes.

Exemple de la classe point

#class point x_init y_init =
object
  val mutable x = x_init
  val mutable y = y_init
  method get_x = x
  method get_y = y
  method set_x z = x <- z
  method set_y z = y <- z
  method move dx dy = x <- x+dx; y <- y+dy
  method to_string ( ) =
   "(" ^ (string_of_int x) ^ "," ^ (string_of_int y) ^ ")"
end;;

Créer une instance

#let p = new point 2 3;;
val p:point =<obj>
#new point;;
- : int -> int -> point =<fun>

Appeler les méthodes

# p#get_x;;
- : int = 2
# p#move 1 1;;
- : int = ()
# p#to_string();;
- : string = "(3,4)"

La notion d'héritage

#class point_colore x_init y_init (c_init:string) =
object (me)
  inherit point x_init y_init as mother
  val mutable c = c_init
  method get_color = c
  method set_color nc = c <- nc
  method to_string()=mother#to_string() ^ me#get_color
end;;

On remarque que l'argument de couleur de l'objet, à savoir "c_init", a été déclaré avec un type contraint "string". Ceci peut être utile si l'utilisation de l'argument ne permet d'en déterminer le type : une telle indétermination n'est pas acceptée par l'interpréteur. Dans le cas présent, cette contrainte de type est superflue car la méthode "to_string" permet de déterminer le type de "c_init".

Dans l'exemple précédent, il était utile de se référencer soit-même (pour invoquer "get_color"), ainsi que la classe mère (pour invoquer "to_string"). La référence sur soi-même s'obtient en faisant suivre "object" par un identificateur entre paranthèses (ici "me"). La référence sur la classe mère se fait en fin de la déclaration "inherit" en la faisant suivre par "as" et par un identificateur donné (ici "mother").

Voici maintenant comment créer et manipuler une instance de la classe "point_colore" :

#let p = new point_colore 2 3 "bleu";;
val p : point_colore = <obj>
#p#get_color;;
-:string = "bleu"

Liaison retardée

Lorsque des méthodes sont redéfinies dans des classes filles, il est nécessaire de déterminer de quelle méthode il est question lors d'une invocation donnée. On désigne par "liaison retardée", le fait que la détermination de la méthode à utiliser lors d'une invocation est dynamique : elle se fait à l'exécution, en fonction du contexte.

Classe abstraite

Une classe peut être déclarée virtuelle ("class virtual nom = object ... end") lorsque certaines méthodes sont déclarées mais ne possèdent pas de corps. Ces méthodes sont également déclarées virtelles ("method virtual nom : type"). Une telle classe ne peut pas être instanciée (par "new ...") et permet de définir un cadre générique pour ses classes dérivées.

Exemple :

# class virtual printable () =
object(self)
  method virtual to_string : unit -> string
  method print () = print_string (self#to_string())
end ;;
# class rectangle (p1,p2) =
object
  inherit printable ()
  val mutable llc = (p1 : point)
  val mutable ruc = (p2 : point)
  method to_string () = "[" ^ p1#to_string() ^ "," ^ p2#to_string() ^ "]"
end ;;

Les classes paramétrées

Les classes paramétrées mettent en oeuvre le polymorphisme dans la définition des classes.

Exemple :
On désire construire une paire d'éléments. La définition suivante est incorrecte :

#class pair x0 y0 =
object
  val x = x0
  val y = y0
  method fst = x
  method snd = y
end;

La définition précédente est incorrecte car elle conduit à utiliser des paramètres de types (pour les types de x0 et de y0) que le système d'inférence de types ne peut calculer. Les paramètres de type nécessaires doivent alors être explicitement indiqués comme dans la construction suivante qui est correcte :

#class ['a, 'b] pair (x0:'a) (y0:'b) =
object
  val x = x0
  val y = y0
  method fst = x
  method snd = y
end;
On peut alors instancier et utiliser la classe de la manière suivante :
#let p = new pair 2 'x';;
val p : (int, char) pair = <obj>
#p#fst;;
- : int = 2

Autre exemple permettant de construire des piles génériques

#class ['a] pile =
object(this)
  val mutable p : 'a list = []
  method est_vide = (p = [])
  method lire_sommet = List.hd p
  method empiler x = p <- x::p
  method depiler = if (this#est_vide)
      then failwith("pile vide")
      else p <- List.tl p;
  method to_string () = p
end;;

Dérivation dans les classes paramétrées

Voici est exemple :

# class ['a,'b] acc_pair (x0 : 'a) (y0 : 'b) =
object
  inherit ['a,'b] pair x0 y0
  method get1 z = if x = z then y else raise Not_found
  method get2 z = if y = z then x else raise Not_found
end;;
On peut aussi dériver une classe paramétrée pour spécifier ses paramètres, comme dans l'exemple suivant :
# class pair_point (p1,p2) =
object
  inherit [point,point] pair p1 p2
end;;

2. Problème : constructions géométriques

En utilisant la classe point vue dans les rappels de cours, construire un ensemble de classe permettant des représentations géométriques qui sont décrites dans l'arbre d'héritage suivant :



On implémentera les spécifications suivantes :

Ecrire un petit programme testant toutes ces classes.

Compléments (pour les plus rapides) :

En consultant le chapitre 5 du livre d'E. Chailloux et al. "Objective Caml" (accessible en ligne au format html), ajouter à ces classes une gestion graphique mettant en oeuvre le module Graphics.