Programmation modulaire
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

La programmation modulaire est la séparation d'un programme en différents modules, définissant chacun des types de données et des fonctions pour traiter un aspect du programme. Par exemple, on peut définir un module pour le traitement des dates dans une application. Un module peut utiliser d'autres modules. En OCaml, chaque module possède une implémentation et une interface.

L'intérêt de la séparation interface/implémentation est la possibilité de faire évoluer l'implémentation d'un module (correction, optimisation, changement de la représentation des données ou des algorithmes utilisés) de façon transparente pour les modules utilisant ce module, tant que l'interface reste compatible.

Une bibliothèque regroupe souvent plusieurs modules.

Voyons comment sont définis et utilisés les modules en OCaml.

1. Interface et implémentation

L'interface d'un module définit ce que le module offre, ce qui peut être utilisé depuis un autre module. L'implémentation du module doit définir les éléments présents dans l'interface mais peut en définir d'autres, qui ne seront alors visibles qu'à l'intérieur du module. L'interface d'un module est aussi appelée signature.

En OCaml, l'interface d'un module M est définie dans un fichier m.mli (ou M.mli) tandis que son implémentation est définie dans un fichier m.ml (ou M.ml). S'il n'y a pas de fichier d'interface correspondant à un fichier m.ml, alors le compilateur considère que tout ce qui est défini dans l'implémentation est visible dans l'interface, avec les types inférés par le compilateur.

On notera la relation entre le nom d'un module "racine" et le nom des fichiers définissant son interface et son implémentation.

Voyons un exemple de définition d'un module M, avec son interface dans le fichier m.mli et son implémentation dans m.ml:

m.mli
type pair
val x : int

val make_pair : int -> int -> pair
val first : pair -> int
val second : pair -> int
m.ml
type pair = int * int
let x = 1
let y = 2
let make_pair x y = (x,y)
let first (x,_) = x
let second (_,y) = y;;

Dans l'interface, on a rendu le type pair abstrait, en ne mettant que sa déclaration, mais pas la façon dont il est construit. On a également masqué la valeur y mais exposé x en indiquant son type. Le type pair étant abstrait, nous exposons la fonction make_pair pour créer des valeurs du type pair. Enfin, on a exposé les fonctions first et second prenant en paramètre une valeur du type abstrait.

Le passage d'un couple d'entiers (1,2) à la place d'une valeur de type pair sera impossible en dehors du module car la définition du type étant masquée, pair et int * int sont considérés comme deux types non unifiables. Pour ne pas abstraire un type de données, il faut redonner dans l'interface la même définition que dans l'implémentation.

Lorsqu'un module ne contient que la définition de types de données, il n'est pas nécessaire d'avoir un fichier d'implémentation.

2. Compilation séparée

La compilation d'un module dont on a donné explicitement l'interface dans un fichier .mli et l'implémentation dans un fichier .ml commence par la compilation de l'interface:

ocamlc -c m.mli

Un fichier m.cmi est produit, qui est l'interface compilée. Ensuite, l'implémentation est compilée à son tour:

ocamlc -c m.ml

Un fichier .cmo de code-octet est produit. Dans le cas d'une compilation en code natif,

ocamlopt -c m.ml

produit un fichier m.o contenant le code natif compilé et un fichier m.cmx contenant des informations supplémentaires nécessaires au compilateur OCaml lors de l'édition des liens.

Si un fichier m.mli existe mais pas de fichier m.cmi, alors le compilateur signale une erreur indiquant que l'interface doit être compilée d'abord, puisque le code de l'implémentation doit être compatible avec l'interface.

S'il n'y a pas de fichier m.mli, la compilation de m.ml produit à la fois le fichier m.cmo (ou les fichiers m.cmx et m.o) et le fichier d'interface compilée m.cmi. Dans ce cas, tous les éléments de m.ml sont rendus visibles.

3. Accès au module

Une fois le module compilé, on peut l'utiliser si le fichier .cmi est dans le répertoire courant ou ceux indiqués avec l'option -I, et si l'implémentation est chargée (dans le cas de l'interprète) ou liée dans le programme ou la bibliothèque (lorsqu'on utilise ocamlc ou ocamlopt).

Dans le cas de l'interprète, la directive #load permet de charger le code-octet d'un module:

# #load "m.cmo";;

On peut ensuite utiliser les fonctions fournies dans l'interface du module1:

# let p = M.make_pair 1 2;;
val p : M.pair = <abstr>
# M.second p;;
- : int = 2

Pour intégrer ce module dans un programme ou une bibliothèque, on passe le fichier .cmo (ou .cmx) à ocamlc (ou ocamlopt):

cat pair.ml
let p = M.make_pair 1 2;;
M.second p;;
ocamlopt -c pair.ml
ocamlopt -o mon_programme m.cmx pair.cmx

1 Si celui-ci est connu, i.e. le fichier m.cmi est dans le répertoire courant ou indiqué par l'option -I au lancement de l'interprète, ou encore indiqué par la directive #directory "mon_repertoire";; dans l'interprète.