Nous avons vu dans la partie Programmation modulaire que chaque fichier source (ou paire de fichiers .ml/.mli) correspond à la définition d'un module, avec son implémentation et son interface.
Il est possible en OCaml de définir des modules à l'intérieur de ces modules, et plus généralement des modules à l'intérieur de n'importe quel module. Comme pour les valeurs et les types, les sous-modules peuvent apparaître ou non dans la signature du module qui les contient. Ils seront alors visibles ou non dans l'interface du module de plus haut niveau qui les contient.
Pour définir un module, la syntaxe est la suivante:
A l'intérieur de struct ... end, nous pouvons utiliser les constructions que nous pouvons mettre dans un fichier .ml: let globaux, définitions de types, ...
La déclaration d'un sous-module dans l'interface d'un module utilise la syntaxe suivante:
De façon similaire à la construction précédente, à l'intérieur de sig ... end, nous pouvons utiliser les constructions que nous pouvons mettre dans un fichier .mli: déclarations de valeurs avec val, définitions de types, ...
Voyons un exemple, en définissant un module M contenant un module N. Nous plaçons donc l'interface dans le fichier m.mli et l'implémentation dans le fichier m.ml:
Cependant, il est déjà possible de restreindre la signature d'un module dans la partie implémentation, en imposant une contrainte de type:
# module N : sig type t val x : t val foo : t -> t end = struct type t = int let x = 1 let y = 2 let foo x = x + 1 end;; module N : sig type t val x : t val foo : t -> t end
Ce type de contrainte permet de s'assurer par exemple que certains éléments du module N ne sont pas utilisés dans la suite du module conteneur M, puisque les restrictions dûes à l'interface de M ne s'appliquent que pour le code extérieur à M.
Pour les types de données, on peut noter:
ou bien définir un type et l'utiliser par la suite dans les annotations de types:
De façon analogue, il est possible de définir des types de modules, c'est-à-dire de nommer des signatures, via la syntaxe
Les types de module et les modules ont des espaces de noms séparés, il est donc possible d'avoir un type de module M et un module M sans que l'un masque l'autre.
Reprenons notre exemple précédent et déclarons un type de module Mon_type
# module type Mon_type = sig type t val x : t val foo : t -> t end;; module type Mon_type = sig type t val x : t val foo : t -> t end
pour l'utiliser ensuite comme contrainte de signature pour un module N2:
# module N2 : Mon_type = struct type t = int let x = 1 let y = 2 let foo x = x + 1 end;; module N2 : Mon_type
Nous pouvons vérifier que la contrainte empêche bien d'accéder à N2.y:
En allant plus loin, nous pouvons définir des vues différentes sur un même module, par exemple:
# module type Vue1 = sig type t val create : int -> t end;; module type Vue1 = sig type t val create : int -> t end # module type Vue2 = sig type t val read : t -> int end;; module type Vue2 = sig type t val read : t -> int end
Nous créons ensuite un module Base puis deux modules identiques à Base mais dont le type est restreint par nos deux types de module Vue1 et Vue2:
# module Base = struct type t = string let create = string_of_int let read = int_of_string end;; module Base : sig type t = string val create : int -> string val read : string -> int end # module M1 = (Base : Vue1 with type t = Base.t);; module M1 : sig type t = Base.t val create : int -> t end # module M2 = (Base : Vue2 with type t = Base.t);; module M2 : sig type t = Base.t val read : t -> int end # M2.read (M1.create 42);; - : int = 42
La notation Vue1 with type t = Base.t permet d'indiquer une signature correspondant à Vue1 dans laquelle le type t, abstrait dans Vue1, est égal au type Base.t. Cette indication pour M1 et M2 nous permet de déclarer que les types M1.t et M2.t sont égaux, donc de passer une valeur obtenue par M1.create à M2.read.
Ici encore, la contrainte de signature sur M1 nous permet bien d'interdire l'utilisation de M1.read.
Il est possible de définir un type de module à partir d'un module existant, grâce à la construction module type of:
On peut déclarer localement un module, c'est-à-dire le construire en réduisant sa visibilité à une expression:
Le module Bar n'est accessible que dans l'expression située après le in:
les modules construits localement le sont souvent par application de foncteurs.
Il est également possible d'ouvrir localement un module avec la construction let open M in:
# length ;; File "_none_", line 1, characters 0-6: Error: Unbound value length # let open List in length;; - : 'a list -> int = <fun> # length;; File "_none_", line 1, characters 0-6: Error: Unbound value length
Enfin, il existe une autre syntaxe pour ouvrir localement un module, consistant à mettre le nom du module, suivi d'un point et d'une expression entre parenthèses: