Author: Christian.Queinnec@lip6.fr

## Comparaison modules et objets

La principale différence entre programmation modulaire et programmation objet en Objective CAML provient du système de types. En effet la programmation par modules reste dans le système de types à la ML, i.e. du polymorphisme paramétrique (même code exécuté pour différents types de paramètres), alors que la programmation par objets entraîne un polymorphisme ad hoc (où l'envoi d'un message à un objet déclenche l'application de codes différents). Cela est particulièrement clair avec le sous-typage. Cette extension du système de types à la ML ne peut être simulée en ML pur. Il sera toujours impossible de construire des listes hétérogènes sans casser le système de types.
La programmation modulaire et la programmation par objets sont deux réponses sûres (grâce au typage) à l'organisation logique d'un programme, permettent la réutilisabilité et la modifiabilité de composants logiciels. La programmation par objets en Objective CAML autorise le polymorphisme paramétrique (classes paramétrées) et d'inclusion (envoi de messages) grâce à la liaison tardive et au sous-typage, avec des restrictions dues à l'égalité, ce qui facilite une programmation incrémentielle. La programmation modulaire permet de restreindre le polymorphisme paramétrique et utilise la liaison immédiate ce qui peut être utile pour conserver l'efficacité de l'exécution.
Le modèle de la programmation modulaire permet de facilement étendre les fonctionnalités sur des types de données récursifs non extensibles. Si l'on désire ajouter un cas dans un type somme, il sera nécessaire de modifier une bonne partie des sources.
Le modèle de la programmation objet définit un ensemble récursif de données à partir de classes. On prend une classe par cas de données.

#### Efficacité d'exécution

La liaison retardée correspond à une indirection dans le tableau des méthodes (voir page ??). Comme l'accès, de l'extérieur de la classe, à une variable d'instance passe aussi par un envoi de message, cette accumulation d'indirections peut se révéler coûteuse.
Pour montrer cette perte d'efficacité, nous construisons la hiérarchie de classe suivante :

    # class virtual test () =
       object
        method virtual somme : unit -> int
        method virtual somme2 : unit -> int
       end;;
    # class a x  =
       object(self)
        inherit test ()
        val a = x
        method a = a
        method somme () = a
        method somme2 () = self#a
       end;;
    # class b x y  =
       object(self)
       inherit a x as super
        val b = y
        method b = b
        method  somme () = b + a
        method  somme2 () = self#b + super#somme2()
       end;;

On compare ensuite les temps d'exécution entre d'une part l'envoi des messages `somme` `somme2` sur une instance de la classe `b` et d'autre part l'appel à la fonction `f` suivante.

    # let f a b = a + b ;;
    # let iter g a n = for i = 1 to n do ignore(g a) done ; g a ;;
    # let go i j = match i with
                      1 -> iter (fun x -> x#somme()) (new b 1 2) j
                    | 2 -> iter (fun x -> x#somme2()) (new b 1 2) j
                    | 3 -> iter (fun x -> f 1 x) 2 j ;;

    # go (int_of_string (Sys.argv.(1))) (int_of_string (Sys.argv.(2))) ;;

Pour 10 millions d'itérations, on obtient les résultats suivants :

| code-octet | natif
cas 1 | 07,5 s | 0,6 s
cas 2 | 15,0 s | 2,3 s
cas 3 | 06,0 s | 0,3 s

Cet exemple a été construit dans le but de montrer que la liaison retardée a un coût par rapport à la liaison statique habituelle. Ce coût dépend de la quantité de calcul par rapport au nombre d'envois de messages dans une méthode. L'utilisation du compilateur natif diminue la partie calcul sans changer la partie indirection du test. On s'aperçoit dans le cas 2 que les multiples indirections à l'envoi du message `somme2` ont un coût incompressible.

#### Exemple : interface graphique

La bibliothèque graphique `UPI` (voir page ??) a été conçue en utilisant le noyau fonctionnel/impératif du langage. Elle s'adapte très facilement sous forme de modules. Chaque composant devient un module indépendant, permettant ainsi une harmonisation des noms des fonctions. Pour ajouter un composant, il est nécessaire de connaître le type concret des composants. Au nouveau module de modifier les champs nécessaires pour décrire son affichage et ses comportements.
Elle peut aussi se réécrire en objet. Pour cela on construit la hiérarchie de classes de la figure 16.1.

> Figure 16.1 : hiérarchie de classes pour `Upi`

Il est plus aisé d'ajouter de nouveaux composants grâce à l'héritage qu'en utilisant les modules, Néanmoins, l'absence de surcharge nécessite toujours un codage des options en tant que paramètres des méthodes. L'utilisation de la relation de sous-typage permet de facilement construire la liste des composants d'un conteneur. La liaison retardée sélectionne les méthodes propres au composant. L'intérêt de l'objet provient aussi de la possibilité d'étendre ou de modifier le contexte graphique, et les autres types utilisés, toujours grâce à l'héritage. C'est pour cela que les principales bibliothèques graphiques sont organisées selon le modèle objet.

### Traduction de modules en classes

Un module simple ne déclarant qu'un seul type et ne possédant pas de fonction polymorphe indépendante du type déclaré peut se traduire en une classe. Selon la nature du type utilisé (type enregistrement ou type somme) on traduit le module en classe de manière différente.

#### Déclarations de types

##### Type enregistrement

Un type enregistrement s'écrit directement sous forme d'une classe où chaque champ de l'enregistrement devient une variable d'instance.

##### Type somme

Un type somme se traduit en plusieurs classes en utilisant le modèle de conception <<composite>>. Une classe abstraite décrit les opérations (les fonctions) sur ce type. Chaque branche du type somme devient alors une sous-classe de la classe abstraite et implante les méthodes abstraites pour sa branche. Il n'y a plus alors de <<filtrage de motif>> mais choix de la méthode spécifique à la branche.

##### Types paramétrés

Les types paramétrés s'implantent par des classes paramétrées.

##### Types abstraits

On peut considérer une classe comme un type abstrait. À aucun moment l'état interne de la classe n'est visible à l'extérieur de sa hiérarchie. Néanmoins rien n'empêche de définir une sous-classe pour accéder aux variables d'instances d'une classe.

##### Types mutuellement récursifs

Les déclarations de types mutuellement récursifs se traduisent par des déclarations de classes mutuellement récursives.

#### Déclarations de fonctions

Les fonctions dont un paramètre dépend du type t du module se traduisent en méthodes. Les fonctions où n'apparaît pas le type t peuvent être déclarées private dans la mesure où leur appartenance au module n'est pas directement liée au type t. Cela a de plus l'avantage de ne pas poser problème si des variables de types apparaissent dans le type des paramètres. Se pose alors le problème des fonctions dont un paramètre est du type `t` et un autre est du type 'a. Ces fonctions sont très rares dans les modules de la bibliothèque standard. On distingue les modules <<particuliers>> comme `Marshal` ou `Printf` qui ont un typage non standard, des modules sur les structures linéaires comme `List`. Pour ce dernier la fonction `fold_left`, de type ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a, est difficilement traduisible telle quelle dans une méthode de la classe ['b] liste car la variable de type 'a est libre et ne peut apparaître dans le type de la méthode. Plutôt que d'ajouter un paramètre de type à la classe `liste`, il est préférable de sortir ces fonctions dans de nouvelles classes, paramétrées par 2 variables de types et possédant un champ liste.

##### Méthodes binaires

Les méthodes binaires en dehors du sous-typage ne posent pas de difficulté.

##### Autres déclarations

Déclarations de valeurs non fonctionnelles. On peut accepter de déclarer les valeurs non fonctionnelles en dehors des classes. Il en est de même pour les exceptions.

##### Exemple : listes avec itérateur

On cherche à traduire en objet un module ayant la signature `LISTE` suivante :

    # module type LISTE =  sig
       type 'a liste = C0 | C1 of 'a * 'a liste
       val add : 'a liste -> 'a -> 'a liste
       val length : 'a liste -> int
       val hd : 'a liste -> 'a
       val tl : 'a liste -> 'a liste
       val append : 'a liste -> 'a liste -> 'a liste
       val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b liste -> 'a
     end ;;

On déclare tout d'abord la classe abstraite 'a liste correspondant à la définition du type.

    # class virtual ['a] liste () =
       object (self : 'b)
         method virtual add : 'a -> 'a liste
         method virtual empty : unit -> bool
         method virtual hd : 'a
         method virtual tl : 'a liste
         method virtual length : unit -> int
         method virtual append : 'a liste  -> 'a liste
       end ;;

On définit ensuite les deux sous-classes `c1_liste` et `c0_liste` pour chaque composant du type somme. Chacune de ces classes doit définir les méthodes de la classe abstraite ancêtre.

    # class ['a] c1_liste (t, q)  =
       object (self )
         inherit ['a] liste () as super
         val t = t
         val q = q
         method add x = new c1_liste (x, (self : 'a #liste :> 'a liste))
         method empty () = false
         method length () = 1 + q#length()
         method hd = t
         method tl = q
         method append l  = new c1_liste (t,q#append l)
       end ;;
    # class ['a] c0_liste () =
       object (self)
         inherit ['a] liste () as super
         method add x = new c1_liste (x, (self : 'a #liste :> 'a liste))
         method empty () = true
         method length () = 0
         method hd = failwith "c0_liste : hd"
         method tl = failwith "c0_liste : tl"
         method append l  = l
       end ;;
    # let l = new c1_liste (4, new c1_liste (7, new c0_liste ())) ;;
    val l : int liste = <obj>

La fonction `LISTE.fold_left` n'a pas été incorporée dans la classe `liste` pour éviter d'introduire un nouveau paramètre de type. On préfère définir la classe `fold_left` pour implanter ce traitement. Pour cela, on utilise une variable d'instance fonctionnelle (`f`).

    # class virtual ['a,'b] fold_left () =
       object(self)
         method virtual f : 'a -> 'b -> 'a
         method iter r (l : 'b liste) =
           if l#empty() then r else self#iter (self#f r (l#hd)) (l#tl)
       end ;;
    # class ['a,'b] gen_fl f =
       object
         inherit ['a,'b] fold_left ()
         method f = f
       end ;;

On construit alors une instance de la classe `gen_fl` pour l'addition :

    # let afl = new gen_fl (+) ;;
    val afl : (int, int) gen_fl = <obj>
    # afl#iter 0 l ;;
    - : int = 11

### Simulation d'héritage avec des modules

La relation d'héritage entre classes permet de récupérer dans une sous-classe l'ensemble des déclarations de variables et de méthodes de la classe ancêtre. On peut simuler cette relation en utilisant des modules. La sous-classe qui hérite se transforme en un module paramétré dont le paramètre est la classe ancêtre. L'héritage multiple augmente le nombre de paramètres du module. On reprend l'exemple classique sur les points et les points colorés décrit au chapitre 15 pour le traduire en modules.
La classe `point` devient le module `Point` de signature `POINT` suivante.

    # module type POINT =
       sig
         type point
         val new_point : (int * int) -> point
         val get_x : point -> int
         val get_y : point -> int
         val moveto : point -> (int * int) -> unit
         val rmoveto : point -> (int * int) -> unit
         val display : point -> unit
         val distance : point -> float
       end ;;

La classe `point_colore` se transforme en un module paramétré `PointColore` dont le paramètre est de signature `POINT`.

    # module PointColore = functor (P : POINT) ->
     struct
       type point_colore = {p:P.point;c:string}
       let new_point_colore p c = {p=P.new_point p;c=c}
       let get_c self = self.c
     (* début *)
       let get_x self = let super = self.p in P.get_x super
       let get_y self = let super = self.p in P.get_y super
       let moveto self = let super = self.p in P.moveto super
       let rmoveto self = let super = self.p in P.rmoveto super
       let display self = let super = self.p in P.display super
       let distance self = let super = self.p in P.distance super
     (* fin *)
       let display self =
         let super = self.p in P.display super; print_string ("de couleur "^ self.c)
     end ;;

La lourdeur des déclarations <<héritées>> peut être allégée par une procédure automatique de traduction ou une extension du langage. Les déclarations récursives de méthodes pourraient s'écrire par un seul `let rec ... and`. L'héritage multiple entraîne des foncteurs à plusieurs paramètres. Le coût de la redéfinition n'est pas plus grand que la liaison tardive.
La liaison tardive n'est pas implantée dans cette simulation. Pour le faire, il faut définir un enregistrement où chaque champ correspond au type des fonctions/méthodes.

### Limitations de chaque modèle

Le modèle fonctionnel/modulaire offre un cadre rassurant mais rigide pour la modifiabilité du code. Le modèle objet d'Objective CAML souffre de la double vision des classes : structuration et type, impliquant l'absence de surcharge et l'impossibilité d'effectuer des contraintes de type d'un type ancêtre vers un type descendant.

#### Modules

Les principales limitations du modèle fonctionnel/modulaire proviennent de la difficulté d'extensibilité des types. Bien que les types abstraits permettent de s'affranchir de la représentation concrète d'un type, leur utilisation dans les modules paramétrés nécessite d'indiquer à la main les égalités de types entre modules, compliquant l'écriture des signatures.

##### Dépendances récursives

Le graphe de dépendances des modules d'une application ne contient pas de cycle (DAG). Cela implique d'une part de ne pas avoir de types mutuellement récursifs entre deux modules et d'autre part d'empêcher les déclarations de valeurs mutuellement récursives.

##### Difficultés d'écriture des signatures

Un des intérêts de l'inférence de types est de ne pas nécessiter l'écriture des types des paramètres des fonctions. La description des signatures fait perdre cette pratique. Il devient nécessaire de spécifier les types des déclarations de la signature <<à la main>>. On peut utiliser l'option `-i` du compilateur `ocamlc` pour afficher le type de toutes les déclarations globales d'un fichier `.ml` et utiliser cette information pour construire la signature d'un module. Dans ce cas, on perd la démarche <<génie logiciel>> qui consiste à spécifier le module avant de l'implanter. De plus si la signature et le module subissent des changements importants, on doit revenir à l'édition de la signature. Les modules paramétrés ont besoin des signatures de leurs paramètres et celles-ci doivent aussi être décrites manuellement. Enfin si on associe une signature fonctionnelle à un module paramétré, il n'existe pas de possibilité pour récupérer la signature résultat de l'application du foncteur. Cela oblige à écrire principalement des signatures non fonctionnelles, quitte à les assembler par la suite pour construire une signature fonctionnelle.

##### Exportation et importation de modules

L'importation des déclarations d'un module simple s'effectue soit par la notation pointée (`Module.nom`), soit directement par le nom d'une déclaration (`nom`) si le module a été ouvert (open `Module`). Les déclarations de l'interface du module importé ne sont pas directement exportables au niveau du module en cours de définition. Celui-ci a accès à ces déclarations, mais elles ne sont pas considérées comme des déclarations du module. Pour ce faire il est nécessaire de déclarer, à la manière de la simulation d'héritage, les valeurs importées. Il en est de même pour les modules paramétrés. Les déclarations du module paramètre ne sont pas considérées comme des déclarations du module courant.

#### Objets

Les principales limitations du modèle objet d'Objective CAML proviennent du typage.

- pas de méthode contenant un paramètre de type libre;
- difficulté d'échappement du type de la classe dans une de ses méthodes;
- absence de contrainte de type d'ancêtre vers descendant;
- pas de surcharge.

Le point le plus déroutant quand on aborde l'extension objet d'Objective CAML provient de l'impossibilité de construire des méthodes contenant un type paramétré dont le paramètre de type est libre. La déclaration d'une classe peut être vue comme la définition d'un nouveau type et suit en cela la règle générale qui interdit la présence de variables de type libres dans une déclaration de type. C'est pour cela que les classes paramétrées sont indispensables dans le modèle objet d'Objective CAML car elles permettent de lier ces variables de type.

##### Absence de surcharge

Le modèle objet d'Objective CAML ne permet pas la surcharge de méthode. Comme le type d'un objet correspond aux types de ces méthodes, le fait de posséder plusieurs méthodes de même nom mais de types différents entraîne de nombreuses ambiguïtés, dues au polymorphisme paramétrique, que le système ne pourrait résoudre que dynamiquement, ce qui est contradictoire avec la vision totalement statique du typage. Prenons une classe `exemple` qui possède deux méthodes `message` possédant pour la première un paramètre entier et pour la deuxième un paramètre flottant. Soit `e` une instance de cette classe et soit la fonction `f` suivante :

    # let f x a  = x#message a ;;

Les appels `f e 1` et `f e 1.1` ne peuvent pas statiquement être résolus car il n'y a pas dans le code de la fonction `f` d'informations spécifiques à la classe `exemple`.
Une conséquence immédiate de cette absence est l'unicité des constructeurs d'instances. La déclaration d'une classe indique les paramètres à fournir à la fonction de création. Celle-ci est unique.

##### Initialisation

L'initialisation des variables d'instance déclarées dans une classe peut être problématique quand elle doit se calculer à partir des valeurs passées à la construction.

##### Égalité entre instances

La seule égalité qui fonctionne sur les objets est l'égalité physique. L'égalité structurelle retourne toujours `false` quand elle s'applique à deux objets physiquement différents. Cela peut surprendre dans la mesure où deux instances d'une même classe partagent la même table des méthodes. On pourrait imaginer un test physique sur la table des méthodes et un test structurel sur les valeurs (`val`) des objets. Ce sont des choix d'implantation à la manière du filtrage linéaire.

##### Hiérarchie de classes

On ne trouve aucune hiérarchie de classes dans la distribution du langage. En effet l'ensemble des bibliothèques sont fournies sous forme de modules simples ou paramétrés. Cela montre que l'extension objet du langage est encore en cours de stabilisation et milite peu en faveur de son emploi intensif.