Author: Christian.Queinnec@lip6.fr

## Langage des modules simples

Le langage Objective CAML possède un sous-langage de modules qui vient s'ajouter au noyau du langage. Dans ce cadre, l'interface d'un module est appelée sa signature et son implantation est appelée structure. Lorsqu'il n'y a pas d'ambiguïté, nous utiliserons plutôt le terme << module >> pour désigner la structure.
La syntaxe de déclaration des signatures et des structures est la suivante :

### Syntaxe

module type NOM =
sig
déclarations de l'interface
end

### Syntaxe

module Nom =
struct
définition de l'implantation
end

### Warning

Le nom d'un module doit impérativement commencer par une majuscule. Celui d'une signature est libre, mais par convention on utilise des noms en majuscules.

On peut également utiliser les signatures ou des structures anonymes. On écrit alors simplement :

### Syntaxe

sig déclarations end

### Syntaxe

struct définitions end

Nous utiliserons les expressions signature et structure pour désigner soit des noms de signature et de structure, soit leur expression anonyme.
Toute structure a par défaut une signature calculée par l'inférence de types qui reprend l'intégralité des définitions contenues dans la structure. On peut lors de la définition d'une structure, préciser quelle est la signature attendue en rajoutant une contrainte selon l'une des deux syntaxes suivantes :

### Syntaxe

module Nom : signature = structure

### Syntaxe

module Nom = (structure : signature)

Lorsqu'une signature attendue est précisée, le système vérifie que tout ce qui est déclaré dans la signature est défini dans la structure Nom et que les types sont cohérents. En d'autres termes, la signature attendue est incluse dans la signature par défaut. Si tel est le cas, Nom devient un module de signature signature et, de façon analogue à ce qui se passait avec les fichiers d'interface, seules les déclarations apparaissant dans la signature sont accessibles à l'utilisateur du module.
L'accès aux entités déclarées d'un module se fait en utilisant la notation pointée :

### Syntaxe

Nom1.nom2

On dit alors que le nom nom2 est qualifié.
On peut rendre implicite le nom du module en utilisant la directive d'ouverture des modules :

### Syntaxe

open Nom

Dès lors, on peut utiliser les noms des entités sans les qualifier. L'ouverture d'un module provoque, en cas d'identité de nom, le masquage des entités préalablement définies, à la façon des redéfinitions d'identificateurs.

### Deux modules pour les piles

Reprenons les piles en utilisant le langage des modules. Nous commençons par définir la signature d'une pile en reprenant les déclarations du fichier `stack.mli` :

    # module type STACK =
       sig
         type 'a t
         exception Empty
         val create: unit -> 'a t
         val push: 'a -> 'a t -> unit
         val pop: 'a t -> 'a
         val clear : 'a t -> unit
         val length: 'a t -> int
         val iter: ('a -> unit) -> 'a t -> unit
       end ;;
    module type STACK =
      sig
        type 'a t
        exception Empty
        val create : unit -> 'a t
        val push : 'a -> 'a t -> unit
        val pop : 'a t -> 'a
        val clear : 'a t -> unit
        val length : 'a t -> int
        val iter : ('a -> unit) -> 'a t -> unit
      end

On obtient une première implantation des piles en utilisant le module de la bibliothèque standard :

    # module Stack_distrib = Stack ;;
    module Stack_distrib :
      sig
        type 'a t = 'a Stack.t
        exception Empty
        val create : unit -> 'a t
        val push : 'a -> 'a t -> unit
        val pop : 'a t -> 'a
        val clear : 'a t -> unit
        val length : 'a t -> int
        val iter : ('a -> unit) -> 'a t -> unit
      end

On en définit une seconde utilisant des tableaux :

    # module Stack_perso =
       struct
         type 'a t = { mutable sp : int; mutable c : 'a array }
         exception Empty
         let create () = { sp=0 ; c = [||] }
         let clear s = s.sp <- 0; s.c <- [||]
         let size = 5
         let increase s =  s.c <- Array.append s.c (Array.create size s.c.(0))
         let push x s =
           if s.c = [||] then ( s.c <- Array.create size x; s.sp <- succ s.sp )
           else  ( (if s.sp = Array.length s.c then increase s) ;
                   s.c.(s.sp) <- x ;
                   s.sp <- succ s.sp  )
         let pop s = if s.sp =0 then raise Empty
                     else let x = s.c.(s.sp) in s.sp <- pred s.sp ; x
         let length s = s.sp
         let iter f s = for i=0 to pred s.sp do f s.c.(i) done
       end ;;
    module Stack_perso :
      sig
        type 'a t = { mutable sp: int; mutable c: 'a array }
        exception Empty
        val create : unit -> 'a t
        val clear : 'a t -> unit
        val size : int
        val increase : 'a t -> unit
        val push : 'a -> 'a t -> unit
        val pop : 'a t -> 'a
        val length : 'a t -> int
        val iter : ('a -> 'b) -> 'a t -> unit
      end

Les deux modules utilisent un type concret différent pour implanter le type `t`.

    # Stack_distrib.create () ;;
    - : '_a Stack_distrib.t = <abstr>
    # Stack_perso.create () ;;
    - : '_a Stack_perso.t = {Stack_perso.sp=0; Stack_perso.c=[||]}

On retrouve l'abstraction de type en forçant la signature du second module.

    # module Stack_perso = (Stack_perso : STACK) ;;
    module Stack_perso : STACK
    # Stack_perso.create() ;;
    - : '_a Stack_perso.t = <abstr>

Les deux modules `Stack_perso` et `Stack_distrib` n'ont en commun que le nom des fonctions qu'ils implantent. Par contre, leurs types sont différents; il n'est donc pas possible d'utiliser les fonctions de l'un pour manipuler les valeurs de l'autre :

    # let s = Stack_distrib.create() ;;
    val s : '_a Stack_distrib.t = <abstr>
    # Stack_perso.push 0 s ;;
    Characters 19-20:
    This expression has type 'a Stack_distrib.t = 'a Stack.t
    but is here used with type int Stack_perso.t

Même si les deux modules avaient possédé un type t de même implantation, le fait d'abstraire ce type en contraignant le module avec la signature `STACK` interdit la possibilité de partager les valeurs entre les deux modules.

    # module S1 = ( Stack_perso : STACK ) ;;
    module S1 : STACK
    # module S2 = ( Stack_perso : STACK ) ;;
    module S2 : STACK
    # let s = S1.create () ;;
    val s : '_a S1.t = <abstr>
    # S2.push 0 s ;;
    Characters 10-11:
    This expression has type 'a S1.t but is here used with type int S2.t

Objective CAML ne dispose pour vérifier la compatibilité des types que de leur nom (leur implantation étant abstraite) et ici ils sont différents : `S1.t` et `S2.t`. C'est précisément cette restriction qui permet l'abstraction de type en interdisant l'accès à la définition des types dont on veut masquer l'implantation.

### Modules et portée lexicale

Nous donnons dans ce paragraphe deux exemples d'utilisation de signatures pour masquer certaines déclarations.

#### Masquage de types

Abstraire un type permet de restreindre ses valeurs à celles qu'il est possible de construire avec les fonctions que déclare la signature du module où ce type est définit. Dans l'exemple suivant, nous obtenons des entiers dont la construction nous assure qu'ils sont obligatoirement différents de 0.

    # module Int_Star =
       ( struct
           type t = int
           exception Isnul
           let of_int = function 0 -> raise Isnul | n -> n
           let mult = (+)
         end
       :
         sig
           type t
           exception Isnul
           val of_int : int -> t
           val mult : t -> t -> t
         end
       ) ;;
    module Int_Star :
      sig type t exception Isnul val of_int : int -> t val mult : t -> t -> t end

#### Masquage de valeurs

Le masquage d'une valeur permet de réaliser un générateur de symboles analogue à celui vu page ??.
On définit la signature `GENSYM` contenant seulement deux déclarations de fonctions pour la génération de symboles.

    # module type GENSYM =
       sig
         val reset : unit -> unit
         val next : string -> string
       end ;;

On implante ensuite une structure cohérente pour une telle signature :

    # module Gensym : GENSYM =
       struct
         let c = ref 0
         let reset () = c:=0
         let next s = incr c ; s ^ (string_of_int !c)
      end;;
    module Gensym : GENSYM

La référence `c` de la structure `Gensym` n'est pas accessible en dehors des deux fonctions exportées.

    # Gensym.reset();;
    - : unit = ()
    # Gensym.next "T";;
    - : string = "T1"
    # Gensym.next "X";;
    - : string = "X2"
    # Gensym.reset();;
    - : unit = ()
    # Gensym.next "U";;
    - : string = "U1"
    # Gensym.c;;
    Characters 0-8:
    Unbound value Gensym.c

La déclaration de `c` peut être considérée comme locale à la structure module `Gensym` puisqu'elle est masquée par la signature associée au module. La contrainte de signature nous a permis de reproduire plus simplement la définition des fonctions `reset_s` et `new_s` qui utilisaient une déclaration locale (voir page ??).

#### Différentes vues d'un même module

Le langage de module avec contraintes de signature permet d'offrir plusieurs vues d'une même structure. On pourra, par exemple avoir un << super-utilisateur >> du module `Gensym` qui est capable de remettre à jour le compteur et un utilisateur ordinaire qui ne peut que créer un nouveau symbole sans maîtriser le compteur. Pour obtenir ce dernier, il suffit de poser la signature :

    # module type USER_GENSYM =
       sig
         val next : string -> string
       end;;

On crée ensuite le module correspondant par la déclaration :

    # module UserGensym = (Gensym : USER_GENSYM) ;;
    module UserGensym : USER_GENSYM
    # UserGensym.next "U" ;;
    - : string = "U2"
    # UserGensym.reset() ;;
    Characters 0-16:
    Unbound value UserGensym.reset

Pour réaliser ce nouveau module on a réutilisé le module `Gensym`. De plus, les deux modules partagent le même compteur :

    # Gensym.next "U" ;;
    - : string = "U3"
    # Gensym.reset() ;;
    - : unit = ()
    # UserGensym.next "V" ;;
    - : string = "V1"

### Partage de types entre modules

L'incompatibilité entre types abstraits signalée un peu avant (page ??) pose problème lorsque l'on désire partager un type abstrait entre plusieurs modules. Nous examinons deux façons de procéder au partage. L'une est une construction explicite du langage de modules, l'autre utilise la structure de bloc lexical des modules.

#### Partage par contrainte

Illustrons le problème à l'aide du petit exemple suivant. On définit un module `M` qui fournit un type abstrait M.t. Nous le restreignons ensuite selon deux signatures différentes n'autorisant pas les mêmes opérations.

    # module M =
     (
       struct
         type t = int ref
         let create() = ref 0
         let add x = incr x
         let get x = if !x>0 then (decr x; 1) else failwith "Empty"
       end
       :
       sig
         type t
         val create : unit -> t
         val add : t -> unit
         val get : t -> int
       end
     ) ;;

    # module type S1 =
       sig
         type t
         val create : unit -> t
         val add : t -> unit
       end ;;

    # module type S2 =
       sig
         type t
         val get : t -> int
       end ;;
    # module M1 = (M:S1) ;;
    module M1 : S1
    # module M2 = (M:S2) ;;
    module M2 : S2

Pour obtenir l'identification désirée des types M1.t et M2.t, Objective CAML dispose d'une syntaxe pour contraindre un type normalement abstrait dans une signature.

### Syntaxe

NOM with type t1 = t2 and ...

Il s'agit d'une contrainte de type forçant le type t1 déclaré par la signature NOM à être égal au type t2.
On peut poser des contraintes globales sur tous les types d'un module en utilisant la contrainte :

### Syntaxe

with module Nom1 = Nom2

En utilisant de telles contraintes de partage, on peut déclarer les deux modules `M1` et `M2` comme manipulant la même structure de données.

    # module M1 = (M:S1 with type t = M.t) ;;
    module M1 : sig type t = M.t val create : unit -> t val add : t -> unit end
    # module M2 = (M:S2 with type t = M.t) ;;
    module M2 : sig type t = M.t val get : t -> int end
    # let x = M1.create() in M1.add x ;  M2.get x ;;
    - : int = 1

#### Partage et sous-modules

Une autre solution pour assurer le partage de type est d'utiliser le mécanisme des sous-modules. En définissant deux sous-modules (`M1` et `M2`) partageant un type de données abstrait d'un module englobant `M`, nous pouvons parvenir au résultat souhaité.

    # module M =
       ( struct
           type t = int ref
           module M_hide =
             struct
               let create() = ref 0
               let add x = incr x
               let get x = if !x>0 then (decr x; 1) else failwith"Empty"
             end
           module M1 = M_hide
           module M2 = M_hide
         end
        :
         sig
           type t
           module M1 : sig  val create : unit -> t  val add : t -> unit end
           module M2 : sig  val get : t -> int  end
         end ) ;;
    module M :
      sig
        type t
        module M1 : sig val create : unit -> t val add : t -> unit end
        module M2 : sig val get : t -> int end
      end

On obtient bien le résultat voulu et une valeur créée par `M1` peut être manipulée par `M2` :

    # let x = M.M1.create() ;;
    val x : M.t = <abstr>
    # M.M1.add x ; M.M2.get x ;;
    - : int = 1

On rajoute cependant un peu de lourdeur par rapport à la solution précédente : l'accès aux fonctions de `M1` et `M2` se fait via le module englobant `M`.

### Modules simples et extension

Un module est une entité définie une fois pour toutes. En particulier, lorsque nous définissons un type abstrait à l'aide du mécanisme de modules nous ne pouvons plus en étendre les traitements. En particulier, s'il n'a pas été défini de fonction de création, on ne pourra jamais obtenir de valeur de ce type !
Une façon brutale d'augmenter les traitements fournis par un module est d'éditer les sources et de rajouter ce que l'on désire dans la signature et la structure. Mais alors, on n'a plus du tout affaire au même module et toutes les applications qui utilisaient la version originale du module sont à recompiler. Notons cependant que si la redéfinition des composants du module n'a pas modifié les éléments de l'interface originale, il suffit uniquement de recompiler l'ensemble de l'application sans avoir à modifier ce qui avait été écrit.