Classes et modules.
Postscript, PDF Didier Rémy Polytechnique, INRIA
Cours (super, frames) [man]
[doc] [type-index] Exercices
  1. Point de rencontre
  2. Groupement des classes
  3. Constructeurs de classes
  4. Abstraction et héritage
  5. Classes amies
  6. Combinaison intime
  1. Structures de données (*)
    (Listes, concaténation, cellules)
  2. Bank (*)
  3. Ensembles (**)
  4. Polynomes (***)



Résumé

Modules et classes remplissent des rôles différents.
    ·Les modules gèrent l'abstraction de type et de valeur et la paramétrisation par des types et des valeurs en gros.
    ·Les classes n'offrent pas d'abstraction de type, ni la possibilité de groupement.
    ·Elles offrent le mécanisme de liaison tardive (classes), l'héritage (classe) et bien sûr la programmation par envoi de message (objets).
Il est fréquent de combiner les deux mécanismes pour bénéficier de tous les atouts simultanément.

Point de rencontre des classes et des modules

Quelques exemples s'expriment aussi facilement dans le langage des classes que dans celui des modules.

L'intersection est à la fois grande et petite:
    ·De nombreux programmes peuvent être écrits dans un style ou dans l'autre.
    ·Cependant, la modularité n'est pas toujours la même selon le style utilisé.
    ·Pour des exemples peu modulaire et qui utilisent peu le mécanisme à objet, le choix du style n'est pas fondamentale... au point que ces exemples peuvent également être écrit dans le langage de base.


Point de rencontre: types abstraits algébriques...

L'exemple le plus typique est de point de rencontre est l'implémentation d'un type abstrait algébrique, ... avec modification en place et des opérations unaires!
class ['a] stack = object 

  val mutable p : 'a list = []
  method push v = p <- v :: p
  method pop =
    match p with
    | h :: t -> p <- t; h
    | [] -> failwith "Empty"
end;;
 
module Stack : STACK = struct
  type 'a t = 'a list ref
  let create () = ref []
  let push x s = s := x :: !s
  let pop s =
    match !s with
      h::t -> s := t; h
    | [] -> failwith "Empty"
end;;
Résumé classe    module   
Le type des piles 'a stack    'a Stack.t   
Création d'une pile new stack    Stack.create ()   
Empiler x sur s s#push x    Stack.push x s   
Dépiler s s#pop    Stack.pop s   

Extensibilité

class ['a] stack_ext =
  object (self) 
    inherit ['a] stack



    method top = 
      let s = self#pop in 
      self#push s; s 
end;;
 
module StackExt = struct
  (* include Stack *)
  type 'a t = 'a Stack.t
  let create = Stack.create
  let push = Stack.push
  let pop = Stack.pop
  let top p =
    let s = pop p in
    push s p; s
end;;

Cependant, les modules ne permettent pas la liaison tardive (important si les fonctions sont définies récursivement). Par exemple, la redéfinition de pop n'affectera pas le comportement de top.

Quelle approche choisir?


C'est une affaire de goût quand les deux sont possibles.

Classes quand la liaison tardive est nécessaire.

Modules s'il y a des méthodes binaires or deux types abstraits liés, eg. vecteurs et matrices (leurs représentations doivent être partagées).

Approche combinée dans de nombreux cas.
    ·utilisation orthogonale: chacun son rôle, indépendemment.
    ·combinaison intime: l'un complète l'autre.


Regroupement les classes liées

C'est une utilisation orthogonale de modules et des classes.

Les classes nil et cons sont liées, mais seulement par l'usage.
module Liste = struct
  exception Nil
  class ['a] nil =
    object (self : 'alist)
      method hd : 'a = raise Nil
      method tl : 'alist = raise Nil
      method null = true
    end

 

  class ['a] cons h t =
    object (_ : 'alist)
      val hd = h val tl = t
      method hd : 'a = h
      method tl : 'alist = t
      method null = false
    end
end;;
NOTE: En Ocaml, les listes sont plus naturellement représentées par un type somme qui permet en outre d'utiliser le filtrage.

Extensibilité de classes liées

Typiquement, les deux classes seront étendues simultanément (bien que cela ne soit pas obligatoire)
module Liste1 =  struct 
  class ['a] nil = 
    object 
      inherit ['a] Liste.nil
      method length = 0 
    end
      ;;

 
  
class ['a] cons h t =               
  object                            
    inherit ['a] Liste.cons h t      
    method length =
      1+tl#length 
  end
end;;
On peut aussi étendre les listes par l'ajout d'un nouveau constructeur append, ce qui revient à ajouter une nouvelle classe avec la même interface.

Voir exercice.

Exercice (structures de données)

Exercise 1   Reprendre le module des listes en fournissant d'abord une classe virtuelle pour les différents types de listes.
Answer
Implémenter une classe append prenant deux listes et se comportant comme une liste.
Answer
Montrer que les listes peuvent être définies à partir d'une classe ['a,'bcellule implémentant une cellule à deux cases (non homogènes) et d'une classe ['a, 'bnulle ayant la même interface mais implémentant le pointeur vide. Retrouver les classes nil et cons comme des spécialisations des classes nulle et cellule.
Answer

Constructeurs de classes

En Ocaml, on ne peut créer un objet d'une classe que par la construction new. Pour obtenir plusieurs constructeurs de la même classe, on peut
    ·utiliser des fonctions auxiliaires de construction.
  Celles-ci ne peuvent pas être héritées.    
    ·utiliser des classes auxiliaires.
  Ells peuvent être héritées, mais séparément.    
    ·utiliser des wrappers?
  Ils ne peuvent pas partager les variables d'instance.    
    ·mettre les constructeurs dans des méthodes privées et utiliser les clauses initializers pour les sélectionner.

    ·-1em
utiliser un seul constructeur en utilisant des arguments optionnels nommés ou étiquettés.


Utilisation de fonctions auxiliaires


On définit une classe générale
class c x0 y0 = 
  object (* class principale *)
    val x = x0
    val y = y0
    method peu_importe = if x then y else 0
  end;;
puis des fonctions de création d'objets de cette classe.
let c0 () = new c false 0
and c1 b = new c b 0
and c2 b x = new c (b && x > 0) (x * x);;
let p = c2 true 1 and q = c0();;
Ces constructeurs ne peuvent pas être hérités.

Utilisation de classes auxilliaires


On définit des variantes de la classe générale
class c0 = c false 0
class c1 b = c b 0
class c2 b z = c (b && z > 0) (z * z);;
let p = new c2 true 1 and q = new c0;;
Les classes utilisées comme constructeurs peuvent être héritées:
class c2' b x = object
  inherit c2 b x 
  initializer Printf.printf "générale3_val_y_=_%d\n" y
end;;
let _ = new c2' true 3;;
Ces constructeurs peuvent être hérités, mais individuellement, sans partage du code ajouté.

Utilisation d'un wrapper


Contrainte les fonctionnalités ajoutées n'accèdent pas aux variables d'instances...

Une classe et ses constructeurs
class wc = object
  method une_autre = Printf.printf "indépendante"
class c0' = object inherit c inherit une_autre end
class c1' x = object inherit c x inherit une_autre end
Situation idéale... mais assez rare

Utilisation de méthodes privées

On met les constructeurs dans la classe sous forme de méthodes privées qui seront hérités. On utiliser les initializer pour appeler tel ou tel constructeur. Cela fonctionne bien pour des champs mutables définis avec des valeurs par défaut.
class c = object
  val mutable x = 0
  method private c0 = x <- 10
  method private c1 x0 = x <- x0
end;;
class c0 = object (s) inherit c initializer s#c0 end
class c1 z = object (s) inherit c initializer s#c1 z end;;
On peut hériter des constructeurs,
mais pas changer leur type.


Méthodes privées avec héritage
class c' = object
  inherit c as s
  val mutable y = 0
  method private c0 = s#c0; y <- 100
  method private c1' x0 y0 = s#c1 x0; y <- y0
end;;
class c0 = object (s) inherit c' initializer s#c0 end
class c1 x y =
 object (s) inherit c' initializer s#c1' x y end;;

Constructeur unique


Le principe: On distingue les différents cas par un type somme.
type arg_c = Zéro | Un of bool | Deux of bool * int
class c args =
  let x0, y0 = match args with
    | Zéro -> false, 0
    | Un b -> b, 0
    | Deux (b,x) -> b, x in
  object 
    val x = x0 val y = y0
    method peu_importe = if x then y else 0
  end ;;


Problème c'est très lourd...

Solution Arguments (nommés) optionnels ou étiquettés.

Class avec arguments étiquetés

Voir la construction correspondante de Ocaml.

Ils évitent la déclaration de types concrets éphémères:
class point arg =
  let x0 =
    match arg with `Int x -> float x | `Float x -> x in
  object 
    val x = x0
    method getx = x
  end ;;
let p = new point (`Int 2);;
let q = new point (`Float 2.5);;

Classe avec arguments optionnels

Voir la construction correspondante de Ocaml.
class c ?b:(x0 = false) ?x:(y0 = 0) =
  object 
    val x = x0 val y = y0
    method peu_importe = if x then y else 0
  end ;;


Héritage avec arguments optionnels
class c' ?b ?x = object 
  inherit c ?b ?x
  initializer Printf.printf "new_c'_with_val_y_=_%d\n" y
end ;;
let p = new c and q = new c true 3;;

Exercise 2  [Constructeurs]   On reprend l'exemple de la banque mais en style objet style objet, afin de rester extensible. En particulier, on veut pouvoir raffiner le modèle de la banque tout en héritant des constructeurs (i.e. comment spécialiser la version de la banque et du client simultanément)?

La banque peut regrouper un ensemble de services financiers liés dans un même module
Compte.

Définir une classe
compte_bancaire utilisée pour des extensions ultérieures. (On pourra reprendre, par exemple, la classe compte_avec_relevé Elle sera cachée dans la vue montrée au client.

Définir un type de classe
VUE_DU_CLIENT.

Définir une classe
client permettant au client de gérer ses comptes (sans pouvoir en créer) et si besoin d'hériter de cette classe pour personnaliser la gestion de son compte.

Définir une fonction de construction
promotion permettant une offre promotionnelle, non modifiable, (qui ne pourra donc pas être héritée).
Answer
Le client spécialise sa vision du compte client. Par exemple, founrnir une version dérivée de la classe client lui permettant de gérer lui-même ses comptes (indépendemment du relevé de la banque). Cette classe prend en paramètre un compte auxilliaire servant d'épargne et sur lequel sont effectuer automatique des dépôts lorsque le solde dépasse une valeur limite.
Answer

Abstraction et héritage

La combinaison des modules et des classes devient incontounable lorsqu'il y a un besoin simultané d'abstraction et d'héritage:
    ·Les modules réalisent l'abstration par excellence.
    ·Les classes permettent l'héritage avec son mécanisme de liaison tardive.


Fonctions et classes amies

Le fait de cacher les variables d'instances dans les objets fournit un mécanisme d'abstraction de valeur. On peut ajuster cette abstraction en rendant visible en écriture ou en lecture certaines parties par des méthodes appropriées.

Toutefois, l'abstraction offerte par les objets est du tout ou rien:
    ·Tous les objets ont accès à la partie rendue visible.
    ·Aucun autre objet que self n'a accès à la partie restant cachée.

Pas même les objets de la même classe.
Une solution: avoir des amis de confiance:
    ·rendre la représentation visible pour comminiquer entre amis
    ·rendre la représentation abstraite (les amis ont la même vue) pour la sécurité, en utilisation les modules.


Les objets amis d'une même monnaie

module type MONNAIE = sig
  type t
  class m : float -> object ('a)
    method plus : 'a -> 'a
    method prod : float -> 'a method v : t
  end
end
module Monnaie = struct
  type t = float
  class m x = object (_ : 'a)
    val v = x method v = v
    method plus(z:'a) = {< v = v +. z#v >}
    method prod x = {< v = x *. v >}
  end
end;;

Objets amis via l'abstraction des modules
La solution repose entièrement sur les modules et fonctionne comme dans la version modulaire de la monnaie
module Euro = (Monnaie : MONNAIE);;
let cent = new Euro.m 100.0;;
let deux_cents = cent # prod 2.0;;

module Dollar = (Monnaie : MONNAIE);;
let box = new Dollar.m 1.0;;

box # plus deux_cents;;

Un autre exemple:
    ·Les ensembles avec une opération d'union.
    ·Les tables de hachage avec une opération de fusion.


Un exemple de combinaison intime.




Structures Algébriques
(Projet FOCS, Paris 6)


    ·Héritage multiple,
    ·Liaison tardive;
    ·Pas d'encapsulation de l'état;
    ·Les modules, à la place.


Solutions envisagées


À base de classes
    ·Difficulté de l'abstraction de la représentation.
    ·Dissymétrie des méthodes binaires


À base de modules
    ·Absence d'héritage (définition incrémentale)
    ·Absence de liaison tardive (ré-implémentation plus efficace d'une implémentation par défaut).


Solution retenue Mixte
    ·Les classes fournissent l'héritage et la liaison tardive.
    ·Les modules fournissent l'abstraction (et aide à l'organisation du code)


Solution combinée

Les structures algébriques peuvent être implémentées de façon incrémentales par de petits enrichissements.

Pour pouvoir étendre et spécialiser une implémentation par défaut, on présente l'ensemble des opérations dans une classe sans variable d'instance (essentielles).

Chaque espèce algébrique peut être présentée dans une structure avec:
  1. Les opérations par défaut fournies dans une classe virtuelle.
  2. Le type des opérations manquantes (type de class)
  3. Le type d'une instance de cette espèces après que toutes les opérations manquantes auront été fournies.
  4. Un wrapper (sous-forme de foncteur) qui assemble les opérations par défaut et les opérations fournies et cache l'implémentation dans le résultat final.
Structures Génériques

Les structures génériques spécifient les opérations de bases et fournissent des opérations dérivées.
class virtual ['a] semi_group =
  object(self)
    method virtual equal: 'a -> 'a -> bool
    method not_equal x y = not (self#equal x y)
    method virtual zero: 'a
    method virtual plus: 'a -> 'a -> 'a
  end;;
Le paramètre 'a est le type de la représentation des éléments de la structure.

Structures générique (héritage)

Les structures génériques sont construites par héritage.
class virtual ['a] group =
  object(self)
    inherit ['a] semi_group
    method virtual opposite: 'a -> 'a
    method minus x y = self#plus x (self#opposite y)
  end;;
Elles utilisent la liaison tardive pour définir ou redéfinir des méthodes spécifiés ou fournies avec une implémentation par défaut.

Structures concrètes

Ce sont des instances des structures génériques qui ont des implémentations concrètes pour les opérations de base.

Le groupe des entiers modulo p son implémemtation
class z_pz_impl p = object
    method equal (x : int) y = (x = y)
    method zero = 0
    method plus x y = (x + y) mod p
    method opposite x = p - 1 - x
  end;;
est combinée à la structure abstraite:
class z_pz p = object
    inherit [int] group
    inherit z_pz_impl p
  end;;

Abstraction de la représentation

Par les modules (on ajoute une injection une projection)
module type GROUP = sig 
  type t
  val meth: t group
  val inj: int -> t val proj : t -> int
end;;
module Z_pZ (X: sig val p : int end) : GROUP =
  struct
    type t = int
    let meth = new z_pz 2
    let inj x =
      if x >= 0 && x < X.p then x else failwith "Z_pZ.inj"
    let proj x = x
  end;;

Motif de programmation

On peut améliorer l'exemple en définissant un motif de programmation, ie. en élaborant un modèle d'assemblage des structures génériques et concrètes que reproduite pour chaque type de struture.

On peut aussi augmenter la réutilisabilité en fournissant fournir les opérations sous forme de méthodes privées, ce qui permet des les oublier; elles sont alors rendues publiques juste avant la création d'objets représentant les structures concrètes.

Exercices



Ensemble

Le but de cet exercise est de fournir une version objet la librairie Set. On réalisera donc un module Oset.

Pourquoi la librairie Set est-elle implémentée par un foncteur Make qui retourne une structure?

Donner un exemple d'utilisation d'une fonction de comparaison non trivial.

Pourqui ne peut-on pas confondre un ensemble d'entier et un ensemble d'ensemble d'entiers? Le vérifier sur l'ensemble vide. La librairie Set implémente une version fonctionnelle des ensembles par opposition à version impérative. Qu'est-ce que cela veut dire?

Donner la signature du module Oset (on ne retiendra que les opérations principales (tests, ajout retrait, union, comparaison, iter)

Donner l'implémentation du module Oset (on ne retiendra que les opérations principales (tests, ajout retrait, union, comparaison, iteration)

Quel problème y aurait-il à fournir une méthode fold? où 'a est le type de la structure retournée. La méthode fold devrait être polymorphe. (Abstraire la classe par rapport à 'a est possible mais pas satisfaisant).



Les polynômes

Reprendre la librairie sur les polynômes en mélangeant modules et objets.
    ·Définir les anneaux comme une classe (virtuelle) qui étend la classe des groupes.
    ·Définir les polynômes comme une classe (virtuelle) qui étend la classe des anneaux.
    ·Implémenter les polynômes à coefficients sur un anneau.
Utiliser les modules comme précédemment pour préserver l'abstraction.


This document was translated from LATEX by HEVEA and HACHA.