20

Chapitre 2

Objective ML

Introduction

Dans ce chapitre, nous présentons le langage Objective Caml d’abord par des exemples, puis nous formalisons les aspects objets, sans toutefois entrer dans les détails.

2.1 Introduction à Objective Caml

Objective Caml est un langage à classes^{1}. Les objets ne sont pas créés directement, mais à partir de classes ou par modification d’objets existants. En particulier, les objets créés par modification d’autres objets partagent les mêmes méthodes. Un objet a donc une classe associée, celle à partir de laquelle il a été créé ou celle de l’objet à partir duquel il a été dupliqué. Cette propriété est importante. Comme tout invariant, elle permet d’analyser plus facilement les programmes, mais aussi de mieux les compiler.

Toutefois, les objets existent en temps que valeurs. Il est naturel et plus simple d’autoriser leur création directe. Nous choisirons cette approche pour l’exposition, et nous rendrons la construction correspondante inaccessible à l’utilisateur.

2.1.1 Classes et objets

Un objet est composé d’un ensemble de variables d’instance et d’un ensemble de méthodes. Par exemple, un point sur la droite peut être défini par une variable d’instance x et deux méthodes getx et print :

    let p = object val x = 1 method getx = x method print = print int x end;;

Son type est :

    p : ⟨ getx : int; print : unit ⟩

L’objet p est identique a celui que l’on aurait obtenu à partir d’une définition de classe :

1En anglais on dit aussi class-based language par opposition à object-based language

    class point x =
      val x = x
      method getx = x
      method print = print int x
    end;;
    class point (int) = val x : int method getx : int method print : unit end

La définition de la classe point a trois effects sur l’environnement :

• ajouter la définition à la variable de classe point dans l’environnement des classes.

• ajouter une valeur new point qui permet de créer des objets de la classe correspondante.

• ajouter des abbréviations de type point et #point. La première est égale au type des points ⟨getx : int; print : unit⟩. La seconde est plus générale et s’unifie avec les types de toutes les sous-classes de point.

Vérifions que l’on peut créer de nouveaux points et leur envoyer des messages :

    let p = new point 1;;
    p#print;;

L’envoi de message est polymorphe, comme le montre l’exemple suivant :

    let send print  p = p#print;;
    val send print : ⟨ print : 'a; .. ⟩ → 'a = ⟨fun⟩

Le type de cette fonction indique que l’argument doit être un objet qui possède une méthode print de type 'a où 'a est la valeur retournée par le message. En particulier send print peu être appliqué à p.

2.1.2 Héritage

Les points colorés peuvent être définis à partir des points par héritage :

    class point coloré x c =
      inherit point x as parent
      val color = c
      method getc = color
      method print = parent#print; print string color
    end;;

L’héritage a le même effet que de recopier dans la classe définie toutes les définitions de la classe héritée. La classe parente peut être liée à une variable comme parent dans l’exemple ci-dessus. Une variable liant une classe parente n’est pas une variable ordinaire ; elle ne peut être utilisée que pour envoyer des messages (comme dans parent#print). L’envoi d’un message m_{1} à une classe parente P par une méthode m_{2} définie dans une classe C revient à appliquer à l’objet exécutant la méthode m_{2} la méthode m_{1} définie dans la classe P, au lieu de sa valeur prise dans l’objet exécutant m_{2}.

L’héritage peut aussi être multiple. Par exemple, définissons une classe couleur :

    class couleur c =
      val color = c
      method getc = color
      method print = print string color
    end;;

Les points colorés peuvent alors être défini par héritage multiple des classes point et

couleur.
    class point coloré x c =
      inherit point x as point
      inherit couleur c as couleur
      method print = point#print; couleur#print
    end;;

Les deux classes sont bien sûr équivalentes.

    let p = new point coloré 2 "blanc cassé" in p#print;;

2.1.3 Auto-référence

L’auto-référence est la possibilité pour une méthode de référencer l’objet qui a provoqué l’exécution de cette méthode. Cet objet est en général lié à la variable self ; en Objective Caml la liaison de cet objet à une variable doit être explicite et utilise la construction as. L’auto-référence est le plus souvent utilisée pour envoyer d’autres messages à self ; en quelque sorte, la méthode exécutée délégue une partie du calcul à d’autres méthodes. Par exemple, on aurait pu définir la classe point ainsi :

    class point u as moi m^eme =
      val x = u
      method getx = x
      method print = print int moi m^eme#getx
    end;;

Les points créés par cette classe auront le même comportement que ceux créés par l’ancienne classe. Toutefois, la nouvelle classe est différente. En particulier, si une autre classe en hérite et puis modifie la définition de la méthode getx, alors la méthode print subit l’effet de cette modification, alors qu’elle resterait inchangée si la classe héritait de l’ancienne définition. Cet effet lié à l’envoi de message à l’objet auto-référencé est appelé liaison tardive.

Un autre usage de l’auto-référence est la possibilité de retourner l’objet lui-même en résultat. On peut en effet écrire une classe :

    class moi m^eme() as moi m^eme =

      method moi m^eme = moi m^eme
    end;;
    class moi m^eme (unit) : 'a = method moi m^eme : 'a end

Exercice 2 Que retourne le programme suivant

    let p = new moi m^eme () in p # moi m^eme # moi m^eme;;

Objective Caml fournit une construction pour retourner une copie de l’objet dans laquelle certaines variables d’instance ont été modifiées. Par exemple, un entier peut être représenté par :

    class entier () =
      val n = 0
      method nul () = (n = 0)
      method add p = {⟨ n = n + p ⟩}
    end;;

A chaque appel, la méthode add crée un nouvel entier dont la variable d’instance n est incrémenté de un. Il y a donc deux façons de créer des objets. La façon habituelle consiste à demander un nouvel objet de la classe

    let p = new point 1;;

Cette solution est générale et s’applique toujours possible. On peut également créer un objet de la classe C par reproduction d’un objet déjà construit de la classe C à condition que la classe C ait elle-même définit une méthode de reproduction. Par exemple, on peut étendre la classe point avec une méthode de reproduction :

    class point neuf x =
      inherit point x
      method neuf y = {⟨ x = y ⟩}
    end;;
    let un point = new point neuf 0;;

Il est alors équivalent d’écrire new point ou un point.neuf. La création d’un objet par reproduction n’est intéressante que lorsque, comme dans l’exemple des entiers, la reproduction initialise les variables d’instances à des valeurs dépendant de leur ancienne valeur.

2.1.4 Style impératif

Jusqu’à maintenant, nous avons défini des objets en style purement applicatif. Il est bien sûr possible de programmer avec des objets dans un style impératif. Évidemment, il est possible d’utiliser des références à l’intérieur des objets.

    class point mutable x as moi =

      val x = ref x
      method getx = !x
      method setx y = x := y
      method print = print int moi#getx
    end;;

Le champ x n’est pas mutable mais il contient un objet mutable (une référence). En particulier, il est de type int ref. Une façon plus directe consiste à rendre ce champ mutable sans passer par une référence intermédiaire.

    class point mutable u as moi =
      val mutable x = u
      method getx = x
      method setx y = x ← y
      method print = print int moi#getx
    end;;

Le champ x est alors de type int. De façon plus concise, on aurait aussi pu étendre et modifier la classe point :

    class point mutable u =
      inherit point u
      val mutable x
      method setx y = x ← y
    end;;

Il existe un module Oo qui fournit une fonction copy qui permet de duppliquer un objet : Oo.copy p retourne une copie superficielle de l’objet p. Les variables mutables ont les mêmes valeurs que celle de l’originale, mais elles seront mutées indépendemment.

    Oo.copy;;
    − : (⟨ .. ⟩ as 'a) → 'a = ⟨fun⟩

Cette fonction peut-être utilisée pour duppliquer un objet :

Exercice 3 Écrire une classe copie qui fournit une méthode copy qui dupplique l’objet à qui elle est envoyée. Quand la méthode copy est-elle intéressante ?

Exercice 4 La fonction Oo.copy n’effectue qu’une copie superficielle de l’objet. Comment peut-on effectuer une copie profonde ? Donner un exemple.

Exercice 5 Il existe deux façons de représenter les points en style applicatif et en style impératif. Les comparer.

Exercice 6 Pour les points écrits en style impératif, il existe deux façons de duppliquer un objet. Les comparer.

2.1.5 Classes paramétriques

Les références peuvent aussi être implémentées par une classe. Toutefois, le programme suivant échoue :

class ref x =
val mutable x = x
method get = x
method set y = x ← y
end;;

En effet, les méthodes sont polymorphes, et la classe ref est naturellemnt paramétrique. Il est bien sûr possible d’écrire une version monomorphe :

class ref x =
val mutable x = (x:int)
method get = x
method set y = x ← y
end;;
class ref (int) =
val mutable x : int
method get : int
method set : int → unit
end

Pour écrire une classe paramétrique, il est nécessaire d’indiquer explicitement les variables de types dont elle dépend. Il faut également que ces variables soient liées dans le corps de la définition :

class 'a ref x =
val mutable x = (x:'a)
method get = x
method set y = x ← y
end;;
class 'a ref ('a) =
val mutable x : 'a
method get : 'a
method set : 'a → unit
end

Dans un définition de classe un paramètre de type peut devoir être instancié par un type plus précis. Dans le type de la class, cela est alors indiqué par une clause constraint.

class 'a ref (x:'a) =
val mutable x = x + 1
method get = x method set y = x ← y
end;;

class 'a ref ('a) =
  constraint 'a = int
  val mutable x : int
  method get : int
  method set : int → unit
end

Une constrainte peut également être mise explicitement :

class 'a ref (x:'a) =
  constraint 'a = 'b list
  val mutable x = x
  method get = x method set y = x ← y
end;;
class 'a ref ('a) =
  constraint 'a = 'b list
  val mutable x : 'a
  method get : 'a
  method set : 'a → unit
end

2.1.6 Coercion

Il est parfois possible de coercer un objet vers un autre objet. Par exemple un point coloré peut aussi être considéré comme un point. On écrit cela

let p = new point 1 and q = new point coloré 2 "blanc cassé";;
val p : point = ⟨obj⟩
val q : point coloré = ⟨obj⟩
let pq = [p ; (q :point coloré :⟩ point)];;
val pq : point list = [⟨obj⟩; ⟨obj⟩]

La coercion d’une expression a de type τ_{1} vers le type τ_{2} est possible si l’expression a peut être considéré de type τ_{2} en toute sécurité. Nous verrons dans le chapitre suivant qu’il en est ainsi lorsque les types τ_{1} et τ_{2} sont dans une relation de sous-typage que nous définirons formellement.

Dans bien des cas, il est commode de ne pas indiquer seulement le type du domaine de la coercion. Pour cela un ensemble de primitives ( :> τ) sont founies, par exemple :

fun x → (x :⟩ point);;
− : ⟨ getx : int; print : unit; .. ⟩ → point = ⟨fun⟩

Cependant, le type du domaine est calculé à partir du type du co-domaine, et il n’est pas vrai que (q :> t) soit toujours bien typé si (q : τ′ :> τ) pour un certain τ′ la seconde forme étant plus générale.

Cela ne doit pas être considéré comme une coercions, mais comme une primitive avec un type fixé et la construction (q :> τ) doit être considéré comme l’application de cette fonction à q. Dans le cas présent, il se trouve que cette application est bien typé si q est un point coloré.

La coercion ne modifie pas la valeur coercée. En particulier, un point coloré peut être considéré comme un point, mais l’envoi d’un message à un point coloré vu comme un point aura le même effet que l’envoi du même message au même objet vu comme un point coloré.

La coercion de type est une perte d’information. Une fois cette information oubliée, il n’est plus possible de revenir en arrière. Par exemple un point coloré vu comme un point ne plus être considéré comme un point coloré à nouveau, sauf à avoir gardé un pointeur sur le même objet sans perte d’information, comme le montre le programme suivant.

    let q' = (q :⟩ point) in (q, q');;
    − : point coloré * point = ⟨obj⟩, ⟨obj⟩

Évidemment ! puisque la liaison de q est statique.

Lorsqu’il est nécessaire de réifier ultérieurement un objet vers sa véritable classe, alors on devra utiliser des types concrets :

    type any point = Point of point | Point coloré of point coloré;;
    type any point = Point of point | Point coloré of point coloré
    let pq = [Point p; Point coloré  q];;
    val pq : any point list = [Point ⟨obj⟩; Point coloré ⟨obj⟩]
    let send any f = function Point p → f p | Point coloré q → f (q:⟩point);;
    val send any : (point → 'a) → any point → 'a = ⟨fun⟩
    List.iter (send any (fun x → x#print)) pq;;
    12blanc cassé− : unit = ()

Ceci peut paraître un peu lourd, se produit raisonable peu souvent.

2.1.7 Méthodes virtuelles

Ils est possible de déclarer une méthode d’une classe sans la définir, ni l’hériter. Elle est alors dite virtuelle. Une méthode utlisé mais non déclarée ni héritée est implicitement virtuelle. Une classe qui contient des méthodes virtuelle est elle même virtuelle ; elle doit être annotée par le mot clef virtual, et il n’est pas possible d’en prendre d’instance. Les méthodes virtuelles devront être définies dans une sous-classe.

2.1.8 Variables d’instance privées

Il est possible d’annoter les variables d’instance par la propriété private. Cela a pour effet de les rendre locale à la définition de la classe et donc invisibles dans le type de la classe, et a fortiori dans les sous-classes.

2.2 Génération automatique de types

Les définitions de classe génèrent de abbréviations de types automatiquement. Ces abbréviations utilisent le partage créé dans les types par unification pendant le typage de la classe pour déterminer l’abbrévaition correspondante à la classe, en particulier pour séparer les occurences de self et des paramètres d’autres types qui pourraient leur être accidentellement égaux.

Correction des exercices