Modules et sous-modules
g progmod Programmation modulaire submod Modules et sous-modules progmod->submod foncteurs Foncteurs et réutilisabilité submod->foncteurs fstclassmod Modules de première classe submod->fstclassmod
Table des matières

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.

1. Définition d'un sous-module

Pour définir un module, la syntaxe est la suivante:

module Id = struct
  ...
end

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:

module Id : sig
  ...
end

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:

m.mli
module N : sig
  type t
  val x : int
  val foo : int -> t
end
m.ml
module N = struct
  type t = int
  let x = 1
  let y = 2
  let foo x = x + 1
end

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.

2. Types de modules

Pour les types de données, on peut noter:

val x : int * int;;
val y : (int * int) * (int * int);;

ou bien définir un type et l'utiliser par la suite dans les annotations de types:

type t = int * int;;
val x : t ;;
val y : t * t;;

De façon analogue, il est possible de définir des types de modules, c'est-à-dire de nommer des signatures, via la syntaxe

module type Id = sig
  ...
end

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:

# N2.y;;
Error: Unbound value 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.

# M1.read (M1.create 63);;
Error: Unbound value M1.read
3. Construction module type of
OCaml ≥ 3.12

Il est possible de définir un type de module à partir d'un module existant, grâce à la construction module type of:

# module type T = module type of M1;;
module type T = sig type t = Base.t val create : int -> t end
OCaml ≥ 3.12
4. Modules locaux

On peut déclarer localement un module, c'est-à-dire le construire en réduisant sa visibilité à une expression:

# let module Bar = struct let x = 1 end in Bar.x + 1;;
- : int = 2

Le module Bar n'est accessible que dans l'expression située après le in:

# Bar.x;;
Error: Unbound module Bar

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 ;;
Error: Unbound value length
# let open List in length;;
- : 'a list -> int = <fun>
# length;;
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:

# List.( let l = concat [ [ 1 ; 2 ] ; [ 3 ; 4 ] ; [ 5 ; 6 ] ] in length l );;
- : int = 6