Programmation orientée objet
Table des matières
Préambule

Ce document n'est pas une introduction au paradigme de la programmation orientée objet, mais présente la programmation orientée objet en OCaml. On suppose donc que les notions de classes, objets, méthodes, héritage, etc., sont déjà connues.

1. Introduction

OCaml s'appelait auparavant Objective-Caml, indiquant ainsi qu'il comporte la possibilité de programmation orientée objet.

Cela signifie que le système de types permet de définir des types pour les objets. La définition d'une classe entraîne la création d'un type (de même nom). L'instanciation d'une classe crée donc une valeur du type correspondant à la classe.

Comme les autres types, les types de classes peuvent également être paramétrés (cf. cette section).

La notion de type pour les classes s'accompagne de la notion de sous-typage, ou polymorphisme d'inclusion. Ainsi, un objet A dont le type est un sous-type d'un autre objet B pourra être utilisé en lieu et place de B. Une relation de sous-typage est typiquement engendrée par héritage, comme dans d'autres langages, mais nous verrons qu'elle est indépendante et que c'est un aspect intéressant du langage.

L'une des limites d'OCaml est l'impossibilité de surcharger1 les méthodes d'une classe, pour la même raison qu'il est impossible de surcharger les opérateurs: il ne serait alors plus possible d'inférer automatiquement le type des expressions.

2. Objets

Nous commençons par voir comment créer et manipuler des objets, sans créer de classe. Par la suite, nous verrons comment créer des classes et les instancier.

2.1. Création d'objets

La création d'un objet se fait avec la syntaxe suivante:

object champs end

Les champs de la définition permettent de définir pour l'objet ses héritages, attributs (ou variables d'instance), méthodes, contraintes de types, ainsi qu'un éventuel traitement à effectuer à l'initialisation.

Le code suivant déclare un objet "vide", sans attribut ni méthode:

# object end ;;
- : <  > = <obj>

Créons maintenant un objet un peu plus utile avec une méthode add : int -> int, et un attribut non modifiable foo : int initialisé à 1:

# object
    val foo = 1
    method add x = x + foo
  end;;
- : < add : int -> int > = <obj>

Le toplevel nous indique que la valeur a un type objet (<obj>) et liste les méthodes visibles entre < et >, en l'occurrence la méthode add : int -> int. Lorsqu'elle sera appelée avec un paramètre x entier, la méthode add retournera la valeur du corps de la méthode, ici l'expression x + foo (comme pour les fonctions).

Lors de la création de l'objet, les expressions d'initialisation des attributs ne peuvent faire appel aux valeurs d'autres attributs de l'objet:

# object
    val x = 1
    val y = x + 1
  end;;
Error: The instance variable x
cannot be accessed from the definition of another instance variable

Le code à l'intérieur d'un objet peut accéder aux attributs en utilisant simplement leur identifiant. Ici nous définissons un attribut foo et une méthode foo. Les attributs et méthodes sont dans deux espaces de noms différents. La méthode foo utilise l'attribut foo:

# object
    val foo = 1
    method foo = foo
  end;;
- : < foo : int > = <obj>

Pour appeler une méthode de l'objet depuis l'intérieur de l'objet, il faut donner un nom à l'objet dans lequel on est; cela se fait en indiquant un identifiant entre parenthèses après le mot-clé object, ici self. On peut alors appeler la méthode sur l'objet self:

# let o =
  object(self)
    (* Ne faites pas ça chez vous :-) *)
    method fact n =
      if n <= 0
        then 0
        else if n = 1
          then 1
          else n * self#fact (n-1)
  end;;
val o : < fact : int -> int > = <obj>
# o#fact 5 ;;
- : int = 120
2.2. Utilisation d'objets

L'utilisation d'un objet passe par l'appel de ses méthodes. L'appel de méthode (aussi appelé envoi de message) se fait en utilisant l'opérateur # avec à gauche un objet et à droite le nom de la méthode éventuellement suivi des arguments:

identifiant # méthode [arguments]

Par exemple, nous créons un objet x et appelons la méthode as_string de cet objet:

# let x = object method as_string = "hello world!" end;;
val x : < as_string : string > = <obj>
# x#as_string ;;
- : string = "hello world!"

Contrairement aux valeurs fonctionnelles (ou fonctions), qui doivent avoir au moins un paramètre pour mériter ce nom et retarder l'évaluation jusqu'au moment où un argument est donné, les méthodes sans paramètre ne sont évaluées que lors de leur appel explicite via un objet. En fait, dans le code généré, ce sont des fonctions qui prennent en premier paramètre l'objet en question. La méthode as_string ci-dessus ne prend pas de paramètre mais son corps n'est évalué que lorsqu'elle est appelée sur un objet, c'est-à-dire, dans le code généré, quand un objet lui est passé en paramètre. Lorsqu'on utilise la notation object(self) ... end, on donne justement un nom à ce paramètre implicite qu'est l'objet, et on peut donc en appeler des méthodes.

Avertissement 1: Les attributs ne sont pas accessibles à l'extérieur

Les attributs d'un objet ne sont pas accessibles depuis l'extérieur de cet objet:

# let objet = object val x = 3 end;;
val objet : <  > = <obj>
# objet#x ;;
Error: This expression has type <  >
       It has no method x
# objet.x ;;
Error: Unbound record field x

Il est nécessaire de fournir les accesseurs (méthodes d'accès) pour les attributs auxquels on permet l'accès depuis l'extérieur de l'objet, c'est-à-dire des méthodes se contentant de retourner le contenu d'un attribut. Par convention, ces méthodes portent le même nom que l'attribut dont elles renvoient la valeur, mais ce n'est pas obligatoire:

# let objet = object val x = 3 method x = x end;;
val objet : < x : int > = <obj>
# objet#x ;;
- : int = 3
2.3. Exécution de code à l'initialisation

Le mot-clé initializer permet de spécifier du code qui sera exécuté à la création de l'objet. Ce code est exécuté après initialisation de tous les attributs; il peut donc accéder aux valeurs des attributs et appeler des méthodes de l'objet (en donnant un nom à l'objet, ici encore self).

# let o =
  object(self)
    val message = "hello world!"
    method hello = print_endline message
    initializer
      self#hello
  end;;
hello world!
val o : < hello : unit > = <obj>
# o#hello;;
hello world!
- : unit = ()
2.4. Attributs mutables

Par défaut, un attribut n'est pas modifiable. Mais, de la même façon que pour les types enregistrements, le mot-clé mutable permet d'avoir des attributs modifiables par le code à l'intérieur de l'objet. La modification d'un attribut est réalisée, comme pour les champs d'enregistrements, avec l'opérateur <-:

# let o =
  object
    val mutable cpt = 0
    method incr = cpt <- cpt + 1
    method print = print_endline (string_of_int cpt)
  end;;
val o : < incr : unit; print : unit > = <obj>
# o#incr ;;
- : unit = ()
# o#incr ;;
- : unit = ()
# o#print ;;
2
- : unit = ()

Le compilateur signale une erreur si on tente de modifier un attribut non modifiable:

# object
  val cpt = 0
  method incr = cpt <- cpt + 1
end;;
Error: The instance variable cpt is not mutable
2.5. Méthodes privées

Le mot-clé private permet d'indiquer qu'une méthode est privée, c'est-à-dire qu'elle ne peut être appelée que depuis une autre méthode de la classe (ou dans la section initializer). Le type inféré pour l'objet ne comporte pas les méthodes privées, qui sont donc bien inaccessibles depuis l'extérieur de l'objet:

# let o =
  object(self)
    method private value = 42
    method as_string = string_of_int self#value
  end;;
val o : < as_string : string > = <obj>
# o#as_string ;;
- : string = "42"
# o#value ;;
Error: This expression has type < as_string : string >
       It has no method value
3. Compatibilité des objets, sous-typage

Un objet étant une valeur comme une autre, il peut être passé à ou renvoyé par une fonction.

Le système de types fera les inférences et vérifications nécessaires pour s'assurer qu'un objet n'est pas passé là où son type ne correspond pas à ce qui est attendu. De même, une expression ne pourra renvoyer un objet avec un type ne correspondant pas aux contraintes locales (par exemple deux objets de types incompatibles en résultat des deux branches d'une conditionnelle).

Voyons quelques exemples.

Commençons par définir une fonction qui retournera un objet comportant une unique méthode print : string -> unit. Si le paramètre err de la fonction vaut true, alors la méthode écrira sur la sortie d'erreur, sinon la méthode de l'objet retourné écrira sur la sortie standard:

# let mk_printer err =
  if err then
    object method print msg = prerr_endline ("erreur: "^msg) end
  else
    object method print = print_endline end
;;
val mk_printer : bool -> < print : string -> unit > = <fun>

Comme attendu, cette fonction prend un booléen et retourne un objet.

Si par mégarde les deux objets n'ont pas un type compatible, le compilateur le signale, ici en indiquant que les types des méthodes print ne sont pas compatibles:

# let mk_printer err =
  if err then
    object method print msg = prerr_endline ("erreur: "^msg) end
  else
    object method print = print_int end
;;
Error: This expression has type < print : int -> unit >
       but an expression was expected of type < print : string -> unit >
       Types for method print are incompatible

Munis de notre fonction mk_printer, nous pouvons créer un objet err_printer:

# let err_printer = mk_printer true ;;
val err_printer : < print : string -> unit > = <obj>

Nous pouvons ensuite définir une fonction prenant en paramètre un objet dont nous ne donnons pas le type, mais que nous utilisons dans le corps de notre fonction:

# let hello pr =
  for i = 1 to 3 do pr#print (string_of_int i) done
;;
val hello : < print : string -> 'a; .. > -> unit = <fun>

Le type inféré pour le paramètre pr est un type objet. La seule contrainte est qu'il doit posséder une méthode print : string -> 'a. Les .. dans le type du paramètre signifient que l'objet peut avoir n'importe quelles autres méthodes de n'importe quels types, ce qui compte est la présence d'une méthode print avec un type compatible avec string -> 'a.

Nous pouvons donc logiquement appeler cette fonction en lui passant en argument notre objet err_printer créé plus haut:

# hello err_printer;;
erreur: 1
erreur: 2
erreur: 3
- : unit = ()

Mais nous pouvons également lui passer un objet anonyme créé juste pour un appel:

# hello (mk_printer false);;
1
2
3
- : unit = ()
# hello (object method foo = 1 method print _ = print_endline "coucou" end);;
coucou
coucou
coucou
- : unit = ()

Bien sûr, si l'objet passé en paramètre ne vérifie pas les contraintes de type, le compilateur signale une erreur:

# hello (object method print n = string_of_int n end);;
Error: This expression has type < print : int -> string >
       but an expression was expected of type < print : string -> 'a; .. >
       Types for method print are incompatible

Pour déterminer la compatibilité de deux types objets, la notion de sous-typage est utilisée. On pourra utiliser un objet d'un type t en lieu et place d'un objet de type t' si t est un sous-type de t'. Un type objet t est sous-type d'un type objet t' si toutes les méthodes de t' sont présentes dans t. De plus, pour chacune, son type dans t doit être un sous-type du type de la même méthode dans t'.

Voyons quelques exemples en définissant une fonction dont nous spécifions le type du paramètre pour qu'il requière une méthode m : int -> int tout en permettant à l'objet passé d'avoir d'autres méthodes (grâce aux ..):

# let test (p : < m : int -> int ; .. >) = p#m 1;;
val test : < m : int -> int; .. > -> int = <fun>

Voyons maintenant la compatibilité avec différents objets:

# (* objet vide, invalide car la méthode m requise est absente *)
test (object end);;
Error: This expression has type <  > but an expression was expected of type
         < m : int -> int; .. >
       The first object type has no method m
# (* objet valide, exactement le même type *)
test (object method m x = x + 1 end);;
- : int = 2
# (* objet invalide: m n'a pas le bon type *)
test (object method m x = float x end);;
Error: This expression has type < m : int -> float >
       but an expression was expected of type < m : int -> int; .. >
       Types for method m are incompatible
# (* objet valide: 'a -> 'a est un sous-type de int -> int *)
test (object method m x = x end);;
- : int = 1
# (* objet valide: m du bon type et une méthode
   supplémentaire ne pose pas de problème *)
test (object method m x = x + 1 method m2 = "hello" end);;
- : int = 2

Ce dernier cas ne passe que parce que le type exigé pour le paramètre de la fonction test contient .., indiquant que les méthodes supplémentaires sont acceptées. Sans ces .., le dernier cas ne passe pas:

# let test2 (p : < m : int -> int >) = p#m 1;;
val test2 : < m : int -> int > -> int = <fun>
# test2 (object method m x = x + 1 method m2 = "hello" end);;
Error: This expression has type < m : int -> int; m2 : string >
       but an expression was expected of type < m : int -> int >
       The second object type has no method m2

Un transtypage (cast) est alors nécessaire pour dépouiller l'objet de sa méthode en trop avant de le passer en paramètre. Le transtypage est réalisé avec l'opérateur :>, qui prend un objet à gauche et un type à droite, le tout entre parenthèses:

# let o = object method m x = x + 1 method m2 = "hello" end;;
val o : < m : int -> int; m2 : string > = <obj>
# test2 (o :> < m : int -> int >);;
- : int = 2

Evidemment, dépouiller un objet d'un type t vers un type t' si t n'est pas un sous-type de t' provoque une erreur:

# let o = object method m = string_of_int end;;
val o : < m : int -> string > = <obj>
# (o :> < m : int -> int >);;
Error: Type < m : int -> string > is not a subtype of < m : int -> int > 
       Type string is not a subtype of int

On constate que le système de types d'OCaml ne confond pas sous-typage et héritage, puisqu'il n'est pas nécessaire à deux objets d'hériter d'un ancêtre commun ou de déclarer une interface pour pouvoir être utilisés l'un à la place de l'autre lorsque le contexte le permet. C'est la différence entre typage structurel (comme ici) et typage nominal.

3.1. Objets fonctionnels

On appelle objets fonctionnels les objets qui, plutôt que mettre à jour un état interne lors de l'appel d'une méthode, renvoient une copie d'eux-mêmes avec éventuellement des modifications des valeurs des attributs de l'objet original.

La syntaxe pour construire une copie de l'objet en cours en en changeant certains attributs est la suivante:

{< attribut1 = expr1 ; attribut2 = expr2 ; ... >} 

Par exemple, nous pourrions faire un objet représentant une liste de la façon suivante:

# let l =
  object
    val value = []
    method value = value
    method cons h = {< value = h :: value >}
    method hd = List.hd value
    method tl = List.tl value
    method map f = {< value = List.map f value >}
  end;;
val l :
  < cons : 'b -> 'a; hd : 'b; map : ('b -> 'b) -> 'a; tl : 'b list;
    value : 'b list >
  as 'a = <obj>
# let l1 = l#cons 1 ;;
val l1 :
  < cons : int -> 'a; hd : int; map : (int -> int) -> 'a; tl : int list;
    value : int list >
  as 'a = <obj>
# let l2  = ((l1#cons 2)#cons 3)#cons 4 ;;
val l2 :
  < cons : int -> 'a; hd : int; map : (int -> int) -> 'a; tl : int list;
    value : int list >
  as 'a = <obj>
# let l3 = l2#map (fun x -> x + 1);;
val l3 :
  < cons : int -> 'a; hd : int; map : (int -> int) -> 'a; tl : int list;
    value : int list >
  as 'a = <obj>
# l3#value;;
- : int list = [5; 4; 3; 2]
4. Classes

Nous avons pour l'instant créé des objets sans passer par la définition de classes. Nous avons également vu qu'il était possible de définir des fonctions retournant des objets créés à partir de paramètres.

L'utilisation d'objets créés de cette façon, si elle est pratique dans certains cas, est déconseillée à grande échelle, car elle nuit à la lisibilité et la maintenabilité du logiciel. Etre capable de nommer et catégoriser les objets et les concepts est important pour les comprendre et les manipuler2.

4.1. Déclaration

La déclaration d'une classe prend la forme suivante:

class identifiant [paramètres] = object champs end

L'identifiant se trouve dans le même espace de nom que les types, car la définition d'une classe introduit un nouveau type du même nom.

4.2. Paramètres de classes

Voyons quelques exemples de déclarations. Tout d'abord une classe sans paramètre

# class c1 = object method hello = print_endline "bonjour le monde!" end ;;
class c1 : object method hello : unit end

Ensuite, déclarons une classe c2 avec deux paramètres, x et y, utilisés pour initialiser deux attributs modifiables de mêmes noms. Nous déclarons également deux méthodes as_string et move, cette dernière modifiant les valeurs des deux attributs:

# class c2 x y =
  object
    val mutable x = x
    val mutable y = y
    method as_string = Printf.sprintf "(%d, %d)" x y
    method move new_x new_y = x <- new_x ; y <- new_y
  end;;
class c2 :
  int ->
  int ->
  object
    val mutable x : int
    val mutable y : int
    method as_string : string
    method move : int -> int -> unit
  end

Comme pour les définitions de fonctions, la syntaxe que nous avons utilisée est un raccourci pour une syntaxe plus explicite utilisant le mot-clé fun. Nous pouvons définir une classe c3 identique à c2 de la façon suivante:

# class c3 = fun x -> fun y ->
  object
    val mutable x = x
    val mutable y = y
    method as_string = Printf.sprintf "(%d, %d)" x y
    method move new_x new_y = x <- new_x ; y <- new_y
  end;;
class c3 :
  int ->
  int ->
  object
    val mutable x : int
    val mutable y : int
    method as_string : string
    method move : int -> int -> unit
  end

Cette syntaxe permet d'introduire des calculs intermédiaires avant ou entre les paramètres, lors de l'instanciation. Elle rappelle qu'une classe, en plus d'un type, définit l'équivalent d'une fonction de construction, prenant des paramètres et retournant un objet.

4.3. Instanciation

L'instanciation d'un objet selon une classe est réalisée avec le mot-clé new suivi du nom de la classe et tout ou partie des paramètres éventuels que celle-ci accepte:

# let o1 = new c1 ;;
val o1 : c1 = <obj>
# o1#hello ;;
bonjour le monde!
- : unit = ()
# let objet_c2 = new c2 3 5 ;;
val objet_c2 : c2 = <obj>
# objet_c2#as_string ;;
- : string = "(3, 5)"
# objet_c2#move 25 73 ;;
- : unit = ()
# objet_c2#as_string ;;
- : string = "(25, 73)"

Au passage, on constate que le type indiqué n'est plus de la forme < .. >, mais que par concision le nom de la classe est utilisé (puisque la déclaration d'une classe entraîne la création d'un type du même nom).

Puisque c2 accepte des paramètres, nous pouvons appliquer partiellement l'instanciation, en ne fournissant qu'une partie des paramètres:

# (* application partielle: aucun paramètre n'est donné *)
let mk_c2 = new c2 ;;
val mk_c2 : int -> int -> c2 = <fun>
# (* on donne un paramètre à la fonction retournée *)
let mk_c2_10 = mk_c2 10 ;;
val mk_c2_10 : int -> c2 = <fun>
# (* on fournit le dernier paramètre à la fonction mk_c2_10
  qui retourne alors un objet de type c2 *)
let o = mk_c2_10 20 ;;
val o : c2 = <obj>
# o#as_string ;;
- : string = "(10, 20)"

Pour bien comprendre ce qui est calculé et à quel moment, définissons une classe prenant plusieurs paramètres et insérons entre ces derniers des affichages:

# class c4 =
  let () = print_endline "this message is printed at the class declaration time" in
  fun x ->
    let () = Printf.printf "got x=%d\n%!" x in
    let z = -x in
    let () = Printf.printf "defined z=%d\n%!" z in
    fun y ->
      let () = Printf.printf "got y=%d\n%!" y in
      object
        initializer
          Printf.printf "initializer: x=%d, y=%d, z=%d\n%!" x y z
      end;;
this message is printed at the class declaration time
class c4 : int -> int -> object  end

Le premier affichage est fait dès la déclaration de la classe, car aucun paramètre attendu n'empêche son évaluation. Voyons le résultat d'applications partielles successives:

# let o = new c4 (* o est maintenant une fonction *) ;;
val o : int -> int -> c4 = <fun>
# let o2 = o 1 (* x = 1 et en interne z=-1 *);;
got x=1
defined z=-1
val o2 : int -> c4 = <fun>
# let o3 = o2 10 (* y = 10, tous les paramètres sont fournis, l'objet est créé *);;
got y=10
initializer: x=1, y=10, z=-1
val o3 : c4 = <obj>

La séquence d'instructions (;) est interdite avant la partie object ... end , ce qui nous oblige à utiliser la construction let () = ... in pour réaliser des effets de bord (affichage).

La définition de valeurs entre les paramètres, mais surtout avant le object ... end permet de faire des calculs et opérations avant la création de l'objet. Les résultats de ces calculs peuvent être utilisés dans les méthodes et surtout dans les attributs, pour leur fournir des valeurs d'initialisation. Souvenons-nous que l'initialisation d'un attribut ne peut utiliser la valeur d'un autre attribut lors de la création de l'objet.

4.4. Constructeurs

Il n'y a pas de notion de constructeur pour les objets en OCaml, ou plutôt il n'y a qu'un seul constructeur, appelé par new suivi du nom de la classe.

Il n'y a pas non plus de surcharge. Cependant, les paramètres d'une classe peuvent être utilisés pour l'instancier avec des valeurs différentes selon le contexte. Les paramètres optionnels sont une façon aisée d'obtenir différentes manières d'instancier une classe:

# class point ?(x=0) ?(y=0) () =
  object
    val x = x
    val y = y
    method as_string = Printf.sprintf "(%d, %d)" x y
  end;;
class point :
  ?x:int ->
  ?y:int ->
  unit -> object val x : int val y : int method as_string : string end
# let origin = new point () ;;
val origin : point = <obj>
# let p = new point ~x: 1 ();;
val p : point = <obj>
# let q = new point ~x: 2 ~y: 3 () ;;
val q : point = <obj>
# List.iter (fun o -> print_endline o#as_string) [ origin ; p ; q ];;
(0, 0)
(1, 0)
(2, 3)
- : unit = ()

Le paramètre () est nécessaire dans la déclaration pour que le compilateur puisse distinguer si new point attend ou non des paramètres optionnels. L'application est totale lorsque le paramètre non optionnel est explicitement donné.

4.5. Destructeurs

Il n'y a pas de libération explicite des objets. Ces derniers, comme les autres valeurs OCaml, sont désalloués lorsqu'ils deviennent inaccessibles.

Il est cependant possible, comme pour d'autres valeurs OCaml, d'associer une fonction à appeler lors de la désallocation de l'objet, grâce à la fonction finalise du module Gc. Dans l'exemple ci-dessous, nous créons des objets avec un nom et qui affichent une chaîne lorsqu'ils sont sur le point d'être libérés:

# class myclass s =
  object(self)
    val name = s
    method on_destroy = print_endline (name^" is being destroyed!")
    initializer
      Gc.finalise (fun o -> o#on_destroy) self
  end
;;
class myclass :
  string -> object val name : string method on_destroy : unit end
# for i = 1 to 3 do
    ignore (new myclass ("object "^string_of_int i))
  done;;
- : unit = ()

Nous forçons la récupération des miettes par le garbage collector:

# Gc.major ();;
object 3 is being destroyed!
object 2 is being destroyed!
object 1 is being destroyed!
- : unit = ()
4.6. Exercices
Exercice 1: Implémentation d'une pile d'entiers

Implémenter une pile d'entiers à l'aide d'une classe. L'état de la pile sera naturellement dans un attribut (variable d'instance) modifiable. Les méthodes permettront d'effectuer les actions suivantes: push : int -> unit, pop : int option, peek : int option (retourne l'élément en haut de la pile mais sans la modifier), is_empty : bool.

Nous implémentons une pile d'entiers car nous verrons le polymorphisme plus loin.

# let stack = new stack ;;
val stack : stack = <obj>
# List.iter stack#push [ 1 ; 2 ; 3 ; 4 ; 5 ];;
- : unit = ()
# let rec print stack =
  match stack#pop with
    None -> assert stack#is_empty
  | Some n -> print_endline (string_of_int n); print stack
;;
val print : < is_empty : bool; pop : int option; .. > -> unit = <fun>
# print stack;;
5
4
3
2
1
- : unit = ()
Exercice 2: Implémentation d'une pile avec une classe fonctionnelle

Implémenter une pile d'entiers à l'aide d'une classe "fonctionnelle" fun_stack, i.e. dont les méthodes retournent un nouvel objet au lieu de faire un effet de bord sur l'état interne. La classe aura les méthodes suivantes: push : int -> fun_stack, pop : (int * fun_stack) option, peek : int option (retourne l'élément en haut de la pile mais sans la modifier), is_empty : bool.

# let stack = new fun_stack ;;
val stack : fun_stack = <obj>
# let stack =
  List.fold_left (fun st n -> st#push n) stack [ 1 ; 2 ; 3 ; 4 ; 5 ];;
val stack : fun_stack = <obj>
# let rec print stack =
  match stack#pop with
    None -> assert stack#is_empty
  | Some (n, stack) -> print_endline (string_of_int n); print stack
;;
val print : (< is_empty : bool; pop : (int * 'a) option; .. > as 'a) -> unit =
  <fun>
# print stack;;
5
4
3
2
1
- : unit = ()
4.7. Classes mutuellement récursives

Il peut parfois être utile de définir des classes se connaissant les unes les autres, de façon à ce que chacune puisse instancier les autres. De la même façon que pour les types, il suffit d'utiliser le mot-clé and de la façon suivante:

# class class1 x =
    object
      val x = x
      method make_c2 = new class2 x 10
    end
  and class2 x y =
    object
      method as_string = Printf.sprintf "(%d,%d)" x y
      method c1 = new class1 x
    end;;
class class1 : int -> object val x : int method make_c2 : class2 end
and class2 :
  int -> int -> object method as_string : string method c1 : class1 end
# let objet_c1 = new class1 42 ;;
val objet_c1 : class1 = <obj>
# let objet_c2 = objet_c1#make_c2;;
val objet_c2 : class2 = <obj>
# objet_c2#as_string ;;
- : string = "(42,10)"
5. Héritage

Une classe ou un objet peuvent hériter d'une classe existante. Par contre, il est impossible d'hériter d'un objet.

5.1. Mécanisme d'héritage

L'héritage est introduit par le mot-clé inherit suivi d'un nom de classe et de ses éventuels paramètres. L'exemple suivant, d'un classicisme éhonté, définit une classe point et, héritant de cette dernière, une classe colored_point. Nous ajoutons des affichages dans les blocs initializer afin de montrer l'ordre d'initialisation:

# class point ?(x=0) ?(y=0) () =
  object
    val mutable x = x
    val mutable y = y
    method x = x
    method y = y
    method move new_x new_y = x <- new_x ; y <- new_y
    method as_string = Printf.sprintf "(%d,%d)" x y
    initializer
      Printf.printf "init point with x=%d, y=%d\n%!" x y
  end;;
class point :
  ?x:int ->
  ?y:int ->
  unit ->
  object
    val mutable x : int
    val mutable y : int
    method as_string : string
    method move : int -> int -> unit
    method x : int
    method y : int
  end
# class colored_point ?(color=0x000) ?x ?y () =
  object(self)
    inherit point ?x ?y () as super
    val mutable color = color
    method color = color
    method set_color c = color <- c
    method as_string = Printf.sprintf "%s[%x]" super#as_string color
    method as_point = (self :> point)
    initializer
      Printf.printf "init colored_point with x=%d, y=%d, color=%x\n%!" x y color
  end;;
class colored_point :
  ?color:int ->
  ?x:int ->
  ?y:int ->
  unit ->
  object
    val mutable color : int
    val mutable x : int
    val mutable y : int
    method as_point : point
    method as_string : string
    method color : int
    method move : int -> int -> unit
    method set_color : int -> unit
    method x : int
    method y : int
  end
# let origin = new point () ;;
init point with x=0, y=0
val origin : point = <obj>
# let colored_origin = new colored_point ~color: 0xa00 () ;;
init point with x=0, y=0
init colored_point with x=0, y=0, color=a00
val colored_origin : colored_point = <obj>
# origin#as_string ;;
- : string = "(0,0)"
# colored_origin#as_string ;;
- : string = "(0,0)[a00]"

Le champ inherit point ?x ?y () as super indique que la classe colored_point hérite de la classe point et que, lors de la création de l'objet de la classe colored_point, la partie correspondant à l'héritage de point sera construite avec les paramètres indiqués (?x ?y ()).

De plus, le mot-clé as permet de donner un identifiant pour faire appel aux méthodes de la classe héritée. Nous nous en servons dans la redéfinition de la méthode as_string, en faisant appel à super#as_string.

La classe colored_point contient également une méthode as_point renvoyant l'objet dépouillé pour être du type point (utilisation de l'opérateur de transtypage :>). Attention cependant, les méthodes qu'il porte restent celles de la classe colored_point:

# colored_origin#as_point#as_string ;;
- : string = "(0,0)[a00]"
Avertissement 2: Redéfinition d'attributs et de méthodes

Par défaut, le compilateur n'émet pas d'avertissement lorsqu'une méthode héritée est masquée par la redéfinition de la méthode dans la classe héritante.

Un avertissement (le numéro 7) peut être activé pour éviter de redéfinir par mégarde une méthode. Idem pour un attribut (numéro 13). Pour éviter l'avertissement quand il est activé, on ajoute un ! à val ou method, signifiant que la redéfinition est volontaire:

# #warnings "+7+13";;
# class foo =
  object
    inherit point ~x: 0 ~y: 0 ()
    val! mutable x = 0
    val mutable y = 0
    method x = x
    method! as_string = "foo"
  end;;
Warning 13: the instance variable y is overridden.
The behaviour changed in ocaml 3.10 (previous behaviour was hiding.)
Warning 7: the method x is overridden.
class foo :
  object
    val mutable x : int
    val mutable y : int
    method as_string : string
    method move : int -> int -> unit
    method x : int
    method y : int
  end

La redéfinition d'attributs ou de méthodes doit respecter la définition de la classe héritée: même type et, pour les attributs, même possibilité de modification ou non.

5.2. Héritage multiple

L'héritage multiple est possible, en utilisant plusieurs fois le mot-clé inherit. Les attributs et méthodes sont hérités dans l'ordre d'apparition des inherit, une méthode héritée pouvant être re-définie par une autre méthode héritée plus loin. Idem pour les attributs.

Là encore, on utilisera le ! après le mot-clé inherit pour indiquer que la redéfinition de méthodes et attributs hérités d'une première classe, par ceux d'une seconde classe héritée, est volontaire:

# class t1 = object val value = 1 method p = "class t1" end;;
class t1 : object val value : int method p : string end
# class t2 = object method p = "class t2" end;;
class t2 : object method p : string end
# class t3 = object val value = 42 end;;
class t3 : object val value : int end
# class t4 = object
  inherit t1
  inherit! t2 (* noter le ! *)
  inherit t3 (* l'absence de ! provoque un warning *)
  method value = value
 end;;
Warning 13: the following instance variables are overridden by the class t3 :
  value
The behaviour changed in ocaml 3.10 (previous behaviour was hiding.)
class t4 : object val value : int method p : string method value : int end
# print_endline (new t4)#p ;;
class t2
- : unit = ()
# (new t4)#value ;;
- : int = 42

Voyons comment se comporte un héritage "en diamant":

class c1 =
  object
    val mutable v = 1
    method print = Printf.printf "c1.v = %d" v; print_newline ()
  end;;
class c2 =
  object(self)
    inherit c1 as super
    method set x = v <- x
    method! print =
      super#print ;
      Printf.printf "c2.v = %d" v; print_newline ()
  end;;
class c3 =
  object(self)
    inherit c1 as super
    method set x = v <- x
    method! print =
      super#print ;
      Printf.printf "c3.v = %d" v; print_newline ()
  end;;
class c4 =
  object(self)
    inherit c2 as c2
    inherit! c3 as c3

    method! print =
      c2#print ;
      c3#print ;
      Printf.printf "c4.v = %d" v;
      print_newline ()

    method set_c2 x = c2#set x
    method set_c3 x = c3#set x
  end;;
# let o = new c4 in o#set_c2 2; o#set_c3 3; o#print;;
c1.v = 3
c2.v = 3
c1.v = 3
c3.v = 3
c4.v = 3
- : unit = ()

La classe c1 dont hérite indirectement deux fois la classe c4 n'est donc bien prise en compte qu'une seule fois, l'attribut v est bien partagé entre les deux classes c2 et c3.

6. Classes et méthodes virtuelles

Il est bien sûr possible de définir des méthodes virtuelles, c'est-à-dire des méthodes dont on donne déjà le type, mais dont l'implémentation devra être fournie plus tard, dans une classe héritante. On déclare des méthodes virtuelles à l'aide du mot-clé virtual, et au lieu de donner l'expression du corps de la méthode, on indique son type, comme dans une interface de classe.

A partir du moment où au moins une méthode requise n'est pas définie (donc au moins une méthode est encore virtuelle), la classe doit être déclarée comme virtuelle, toujours à l'aide du mot-clé virtual.

Définissons une classe virtuelle stringable contenant une méthode virtuelle as_string, ainsi qu'une méthode concrète (le contraire de virtuelle) print, qui fait appel à la méthode virtuelle pour afficher la représentation en chaîne sur la sortie standard:

# class virtual stringable =
  object(self)
    method print = print_endline (self#as_string)
    method virtual as_string : string
  end;;
class virtual stringable :
  object method virtual as_string : string method print : unit end

Il est impossible pour l'instant d'instancier cette classe, puisqu'elle est virtuelle:

# let o = new stringable ;;
Error: Cannot instantiate the virtual class stringable

Nous pouvons définir une nouvelle classe, héritant de stringable et implémentant la méthode manquante:

# class my_c x =
  object
    inherit stringable
    method as_string = string_of_int x
  end;;
class my_c : int -> object method as_string : string method print : unit end

Il devient possible d'instancier cette classe:

# let o = new my_c 42 ;;
val o : my_c = <obj>
# o#print ;;
42
- : unit = ()

Il n'est évidemment pas possible de créer un objet avec une méthode virtuelle:

# let o = object method virtual print : unit end ;;
Error: This object has virtual methods.
       The following methods are undefined : print
7. Types, polymorphismes
7.1. Interfaces de classes

Les interfaces de classes sont l'équivalent des signatures pour les modules; on parle en anglais de class types, les types de classes, comme on parle de types de modules.

Une interface de classe liste des attributs et méthodes avec leur type, mais pas leur valeur:

# class type ctype =
  object
    val mutable foo : int
    method as_string : string
  end;;
class type ctype = object val mutable foo : int method as_string : string end

Nous pouvons nous servir d'une telle interface pour restreindre le type d'une classe:

# class ma_classe : ctype =
  object
    val mutable foo = 42
    val gee = "hello"
    method as_string = string_of_int foo
  end;;
class ma_classe : ctype
# let o = new ma_classe;;
val o : ma_classe = <obj>

Si le type de la classe n'est pas un sous-type de l'interface, le compilateur signale une erreur:

# class ma_classe : ctype =
  object
    val mutable foo = "hello"
    val gee = "world"
    method as_string = foo
  end;;
Error: The class type
         object
           val mutable foo : string
           val gee : string
           method as_string : string
         end
       is not matched by the class type ctype
       The class type
         object
           val mutable foo : string
           val gee : string
           method as_string : string
         end
       is not matched by the class type
         object val mutable foo : int method as_string : string end
       The instance variable foo has type string but is expected to have type
         int

Lorsqu'on restreint le type d'un objet, seules les méthodes sont contraintes, car les attributs de l'objet n'apparaissent pas dans son type:

# (* as_string est de type 'a, sous-type de string, donc ok *)
let o = object val mutable foo = 42 method as_string = assert false end;;
val o : < as_string : 'a > = <obj>
# let (o2 : ctype) = o;;
val o2 : ctype = <obj>
# (* l'absence de l'attribut foo n'est pas un problème *)
let o3 = object method as_string = "hello" end;;
val o3 : < as_string : string > = <obj>
# let (o4 : ctype) = o3;;
val o4 : ctype = <obj>
# (* ici, la méthode as_string n'est pas du bon type *)
let o5 = object method as_string = 42 end;;
val o5 : < as_string : int > = <obj>
# let (o6 : ctype) = o5;;
Error: This expression has type < as_string : int >
       but an expression was expected of type ctype
       Types for method as_string are incompatible
Avertissement 3: Masquage des méthodes publiques ou virtuelles

Il n'est pas possible de masquer, à l'aide d'une contrainte de type, les méthodes publiques et les méthodes virtuelles:

# class ma_classe : ctype =
  object
    val mutable foo = 42
    method as_string = string_of_int foo
    method hello = print_endline "hello world!"
  end;;
Error: The class type
         object
           val mutable foo : int
           method as_string : string
           method hello : unit
         end
       is not matched by the class type ctype
       The public method hello cannot be hidden
7.2. Polymorphisme de classe

Comme les types, les définitions de classes peuvent être paramétrées par des variables de type:

class ['a] identifiant ... = object champs end
class ['a, 'b, ...] identifiant ... = object champs end

Le type créé lorsqu'une classe est déclarée est lui aussi paramétré par les mêmes variables de type.

Lorsqu'une classe est paramétrée de cette sorte, il devient possible d'utiliser les variables de type dans les annotations de type dans la définition de la classe. En reprenant notre exemple d'interface objet pour une liste, nous pouvons maintenant définir une classe représentant une liste d'éléments d'un type quelconque:

# class ['a] clist =
  object
    val value = ([] : 'a list)
    method value = value
    method cons h = {< value = h :: value >}
    method hd = List.hd value
    method tl = List.tl value
    method map f = {< value = List.map f value >}
  end;;
class ['a] clist :
  object ('b)
    val value : 'a list
    method cons : 'a -> 'b
    method hd : 'a
    method map : ('a -> 'a) -> 'b
    method tl : 'a list
    method value : 'a list
  end

On remarque que le type indiqué pour la classe contient une annotation de type object('b)... et que ce 'b est utilisé dans le type des méthodes qui retournent une copie de l'objet.

Instancions maintenant cette classe:

# let l = new clist ;;
val l : '_a clist = <obj>

On constate que l'objet l a pour type _'a clist, ce qui signifie qu'il contiendra des éléments d'un type non encore connu, mais que, dès qu'il le sera, il deviendra impossible d'ajouter des éléments d'un autre type à notre liste:

# let l1 = l#cons 1 ;;
val l1 : int clist = <obj>
# let l2  = ((l1#cons 2)#cons 3)#cons 4 ;;
val l2 : int clist = <obj>
# let l3 = l2#map (fun x -> x + 1);;
val l3 : int clist = <obj>
# l3#value;;
- : int list = [5; 4; 3; 2]
# let l4 = l#cons "hello" ;;
Error: This expression has type string but an expression was expected of type
         int

Il est possible de définir une classe paramétrée par un ou plusieurs types, tout en posant des contraintes sur ces types. Ainsi, nous pouvons paramétrer notre classe par un type 'a, et imposer que ce type soit un sous-type de point, grâce au mot-clé constraint. La notation constraint 'a = #point dénote cette contrainte, le # signifiant "tout type sous-type de point, avec éventuellement des méthodes supplémentaires":

# class ['a] circle c radius =
  object
    constraint 'a = #point
    val center = (c : 'a)
    method radius = (radius : float)
    method as_string = Printf.sprintf "%s--%.2f" center#as_string radius
  end;;
class ['a] circle :
  'a ->
  float ->
  object
    constraint 'a = #point
    val center : 'a
    method as_string : string
    method radius : float
  end
# let circ = new circle origin 2.0;;
val circ : point circle = <obj>
# circ#as_string;;
- : string = "(0,0)--2.00"
# let circ2 = new circle colored_origin 3.0 ;;
val circ2 : colored_point circle = <obj>
# circ2#as_string;;
- : string = "(0,0)[a00]--3.00"

Si la contrainte de type n'est pas respectée lorsqu'on passe le centre à la création de l'objet circle, une erreur est signalée:

# let p = object method x = 1  method y = 2  method as_string = "(1,2)" end;;
val p : < as_string : string; x : int; y : int > = <obj>
# let circ = new circle p 2.0;;
Error: This expression has type < as_string : string; x : int; y : int >
       but an expression was expected of type #point
       The first object type has no method move

Lorsqu'on hérite d'une classe paramétrée par des variables de type, il faut indiquer une valeur pour ces variables de type à la suite du mot-clé inherit:

# class string_list =
  object
    inherit [string] clist
  end;;
class string_list :
  object ('a)
    val value : string list
    method cons : string -> 'a
    method hd : string
    method map : (string -> string) -> 'a
    method tl : string list
    method value : string list
  end

On peut bien sûr utiliser des variables de type de la classe en cours de définition:

# class ['a] clist2 =
  object
    inherit ['a] clist
    method length = List.length value
  end;;
class ['a] clist2 :
  object ('b)
    val value : 'a list
    method cons : 'a -> 'b
    method hd : 'a
    method length : int
    method map : ('a -> 'a) -> 'b
    method tl : 'a list
    method value : 'a list
  end

Avec les classes paramétrées par des variables de type, on obtient des constructeurs polymorphes d'objets, mais pas des objets polymorphes, c'est-à-dire que les types des objets instanciés (notamment les types des méthodes) ne contiennent pas de variables de type libres. Nous allons maintenant voir comment définir des objets dont certaines méthodes sont polymorphes.

7.3. Méthodes polymorphes

Lorsqu'on définit un objet, le type de ses méthodes est le plus général possible, tout comme lors de la définition d'une fonction:

# let f x y = [ x ; y ];;
val f : 'a -> 'a -> 'a list = <fun>
# let o = object method f x y = [x ; y] end;;
val o : < f : 'a -> 'a -> 'a list > = <obj>
# o#f 1 2;;
- : int list = [1; 2]
# o#f "hello" "world";;
- : string list = ["hello"; "world"]

A contrario, comme la définition d'une classe donne lieu à la création d'un type, il n'est pas possible de laisser des variables de type libres dans le type de la classe, de la même façon qu'il ne peut y avoir de variable de type libre dans la définition d'un type:

# type t = 'a -> 'a -> 'a list;;
Error: Unbound type parameter 'a
# class c = object method f x y = [x ; y] end;;
Error: Some type variables are unbound in this type:
         class c : object method f : 'a -> 'a -> 'a list end
       The method f has type 'a -> 'a -> 'a list where 'a is unbound

Introduire des paramètres de types ne suffit pas à rendre une méthode polymorphe, car son type sera instancié lors de l'instanciation de la classe. Avec un paramètre de type à notre classe, on a donc un constructeur polymorphe d'objets, mais pas des objets avec des méthodes polymorphes:

# class ['a] c = object method f (x:'a) (y:'a) = [x ; y] end;;
class ['a] c : object method f : 'a -> 'a -> 'a list end
# let o = new c;;
val o : '_a c = <obj>
# o#f 1 2 ;;
- : int list = [1; 2]
# (* le type de o est complètement connu, il n'est plus polymorphe *)
o#f "hello" "world";;
Error: This expression has type string but an expression was expected of type
         int

Pour définir une méthode polymorphe, il faut explicitement indiquer sur quelles variables de son type porte le polymorphisme, en introduisant des quantificateurs dans son type, suivis d'un .. Reprenons la définition de notre classe, qui n'a plus besoin en l'occurrence d'être paramétrée par un type, et utilisons un quantificateur en explicitant le type de la méthode f:

# class c = object
    method f : 'a. 'a -> 'a -> 'a list = fun x y -> [x ; y]
  end
(* le type de la classe montre maintenant bien une méthode polymorphe, avec des
  variables de type libres *) ;;
class c : object method f : 'a -> 'a -> 'a list end
# let o = new c;;
val o : c = <obj>
# o#f 1 2 ;;
- : int list = [1; 2]
# o#f "hello" "world";;
- : string list = ["hello"; "world"]

On peut évidemment quantifier sur plusieurs variables, et mixer paramètres de types de la classe et méthodes polymorphes:

# class ['a] triple (x:'a) =
  object
    method mk :'b 'c . 'b -> 'c -> 'a * 'b * 'c  = fun y z -> (x, y, z)
  end;;
class ['a] triple : 'a -> object method mk : 'b -> 'c -> 'a * 'b * 'c end
# let mk1 = new triple 1 ;;
val mk1 : int triple = <obj>
# mk1#mk "hello" 42 ;;
- : int * string * int = (1, "hello", 42)
# mk1#mk 42 "hello";;
- : int * int * string = (1, 42, "hello")
Exercice 3: Ajout d'une méthode polymorphe

Modifier la définition de la classe clist pour qu'elle prenne en paramètre la valeur initiale de la liste et lui ajouter une méthode fold_left : ('b -> 'a -> 'b) -> 'b -> 'b, utilisant List.fold_left.

# let ma_liste = new clist [1 ; 2 ; 3 ; 4];;
val ma_liste : int clist = <obj>
# let sum = ma_liste#fold_left (+) 0;;
val sum : int = 10
# let concat = ma_liste#fold_left (fun s x -> s^(string_of_int x)) "";;
val concat : string = "1234"
8. Copie d'objets, comparaison
8.1. Copie

Le module Oo contient des fonctions relatives aux objets. La fonction Oo.copy copie un objet en copiant ses attributs et méthodes. Comme son type l'indique, elle prend n'importe quel objet et retourne un objet du même type (une copie, en l'occurrence):

# Oo.copy;;
- : (< .. > as 'a) -> 'a = <fun>

Pour les attributs, il s'agit d'une copie "peu profonde" (shallow copy, tout comme Array.copy): les valeurs des attributs sont partagées, et la mise à jour d'un attribut dans un objet ne modifie pas l'attribut dans l'objet copié. Par contre, la copie ne descend pas dans les références, donc si un attribut contient une référence, modifier la valeur référencée par l'attribut dans un objet revient à modifier la valeur référencée dans l'objet copié.

# let o = object
    val mutable x = 0
    val y = ref 0
    method set_x x2 = x <- x2
    method x = x
    method set_y y2 = y := y2
    method y = !y
  end;;
val o : < set_x : int -> unit; set_y : int -> unit; x : int; y : int > =
  <obj>
# let o2 = Oo.copy o;;
val o2 : < set_x : int -> unit; set_y : int -> unit; x : int; y : int > =
  <obj>
# o#set_x 42 ; o#set_y 1984 ;;
- : unit = ()
# (* x dans o2 n'a pas été modifié *)
o2#x ;;
- : int = 0
# (* la valeur pointée par y a changé dans les deux objets, mais
  pas la référence *)
o2#y ;;
- : int = 1984

Pour copier un objet depuis l'intérieur de lui-même, nous avons vu l'utilisation de la construction {< ... >}. Si aucun attribut n'est spécifié dans cette construction, cela revient à faire une copie de l'objet. Cela signifie que les deux méthodes copy1 et copy2 dans le code suivant sont équivalentes:

# object(self) method copy1 = {< >}  method copy2 = Oo.copy self end;;
- : < copy1 : 'a; copy2 : 'a > as 'a = <obj>
8.2. Comparaison

Il est possible d'utiliser les fonctions de comparaison génériques Pervasives.compare, (=), (<), (<=), ..., pour comparer deux objets. Le résultat ne sera cependant pas celui espéré:

# let o = object val x = 42 end;;
val o : <  > = <obj>
# let o2 = object val x = 42 end;;
val o2 : <  > = <obj>
# let o3 = Oo.copy o2 ;;
val o3 : <  > = <obj>
# o = o2 ;;
- : bool = false
# o2 = o3 ;;
- : bool = false
# o = o;;
- : bool = true

En effet, deux objets sont égaux s'ils ont la même adresse physique.

La relation d'ordre générique est basée sur l'identifiant interne de l'objet (son numéro dans l'ordre de création des objets), que l'on obtient grâce à la fonction Oo.id:

# let obj1 = object val y = "hello" end;;
val obj1 : <  > = <obj>
# let obj2 = object val y = "abc" end;;
val obj2 : <  > = <obj>
# obj1 < obj2;;
- : bool = true
# Oo.id obj1 ;;
- : int = 198
# Oo.id obj2 ;;
- : int = 199

Il convient donc d'implémenter la fonction de comparaison adaptée, en accédant aux attributs des objets à comparer. On implémente parfois ces comparaisons dans des méthodes binaires.

Par ailleurs, les identifiants des objets ne sont pas conservés lors de la sérialisation; de nouveaux identifiants sont regénérés lors de la re-création des objets. Il conviendra donc de s'appuyer sur ces identifiants avec parcimonie.

9. Méthodes binaires

Une méthode binaire est une méthode prenant en paramètre un objet du même type que celui auquel elle est appliquée.

Dans l'exemple ci-dessous, nous définissons une classe integer dont deux méthodes sont binaires, en indiquant que leur paramètre est un objet du même type que la classe:

# class integer (n : int) =
  object (_ : 'a)
     val value = n
     method value = value
     method add (o : 'a) = {< value = n + o#value >}
     method compare (o : 'a) = n - o#value
  end;;
class integer :
  int ->
  object ('a)
    val value : int
    method add : 'a -> 'a
    method compare : 'a -> int
    method value : int
  end

Nous pouvons ensuite instancier cette classe et utiliser la méthode binaire add qui renvoie ici un nouvel objet integer:

# let n1 = new integer 1 ;;
val n1 : integer = <obj>
# let n2 = new integer 2 ;;
val n2 : integer = <obj>
# let n3 = n1#add n2;;
val n3 : integer = <obj>
# n3#value ;;
- : int = 3

Définissons maintenant une classe integer2 héritant de notre classe integer. Nous lui ajoutons une méthode add_int. Ce faisant, deux objets instanciés par ces deux classes n'ont pas le même type, on ne peut donc utiliser un objet de type integer en paramètre de la méthode add d'un objet de type integer2.

# class integer2 n =
  object
    inherit integer n
    method add_int i = {< value = n + i >}
  end;;
class integer2 :
  int ->
  object ('a)
    val value : int
    method add : 'a -> 'a
    method add_int : int -> 'a
    method compare : 'a -> int
    method value : int
  end
# let c1 = new integer2 1 ;;
val c1 : integer2 = <obj>
# c1#add n2 ;;
Error: This expression has type integer
       but an expression was expected of type integer2
       The first object type has no method add_int

Par contre, nous pouvons passer en paramètre un objet de type integer2:

# let c2 = new integer2 2 ;;
val c2 : integer2 = <obj>
# c2#add c2 ;;
- : integer2 = <obj>
10. Conclusion

Comme nous l'avons vu, la programmation orientée objet en OCaml

  • est très souple, grâce à la possibilité de créer des objets sans classe et au fait que la relation de sous-typage ne dépend pas de l'héritage,
  • est sûre, car la partie objets est totalement intégrée au système de type, sans pour autant être lourde grâce à l'inférence de types,
  • offre toujours une grande généricité, grâce aux différents polymorphismes.

1 Par surcharge, nous entendons la définition d'une nouvelle méthode avec le même nom qu'une autre méthode, mais avec un type différent. Nous employons le terme surcharge comme traduction du terme overload et nous parlerons de redéfinition pour traduire override, qui est la définition d'une méthode dans une classe héritant d'une autre classe où la même méthode avec le même type est déjà définie.
2 "Mal nommer les classes, c'est ajouter de la misère au logiciel", disait (presque) Camus.