Précédent Index Suivant

Modules simples

Le langage Objective CAML est distribué avec un certain nombre de modules déjà définis. Nous avons vu leur utilisation au chapitre . Nous allons voir, dans ce paragraphe, comment ont peut réaliser de tels modules.

Compilation séparée

À l'instar des autres langages de programmation modernes, Objective CAML permet de découper un programme en plusieurs fichiers compilables séparément. Un tel fichier doit obligatoirement avoir l'extension .ml. La compilation du fichier nom.ml 1 produit le module Nom2. Les valeurs, types et exceptions définis dans un module peuvent être utilisés dans un autre module soit en utilisant la notation pointée (Module.identificateur), soit en utilisant le mot clé open.
      nom1.ml        nom2.ml
type t1 = { x:int ; y:int } ;; let val1 = { Nom1.x = 1 ; Nom1.y = 2 } ;;
let f1 c = c.x + c.y ;; Nom1.f1 val1 ;;
open Nom1 ;;
f1.val1 ;;
La compilation des deux fichiers peut se faire séparément mais pas indépendament. Pour compiler le second, il faut avoir préalablement compilé l'interface du premier (nous verrons plus en détail ce qu'est une interface dans la suite). Il faut donc compiler en premier lieu le fichier utilisé par le second.

L'édition de lien pour générer l'exécutable se fait comme décrit au chapitre avec la commande ocamlc (sans l'option -c) suivi des fichiers objets. Attention, les fichiers doivent être dans l'ordre de leur dépendance; ceci interdit donc les dépendances croisées entre modules.

Pour générer un exécutable à partir des fichiers nom1.ml et nom2.ml, nous utiliserons les commandes suivantes :
> ocamlc -c nom1.ml
> ocamlc -c nom2.ml 
> ocamlc nom1.cmo nom2.cmo 

Interface et implantation

La distribution d'Objective CAML fournit le module Stack implantant les fonctions principales pour utiliser une pile (LIFO).

# let queue = Stack.create () ;;
val queue : '_a Stack.t = <abstr>
# Stack.push 1 queue ; Stack.push 2 queue ; Stack.push 3 queue ;;
- : unit = ()
# Stack.iter (fun n -> Printf.printf "%d " n) queue ;;
3 2 1 - : unit = ()
Objective CAML étant distribué avec ses sources, nous pouvons regarder comment les piles ont été implantées.

ocaml-2.02/stdlib/stack.ml
type 'a t = { mutable c : 'a list }
exception Empty
let create () = { c = [] }
let clear s = s.c <- []
let push x s = s.c <- x :: s.c
let pop s = match s.c with hd::tl -> s.c <- tl; hd | [] -> raise Empty
let length s = List.length s.c
let iter f s = List.iter f s.c


Nous nous appercevons que le type des piles connu sous le nom de Stack.t est un enregistrement constitué d'un champ modifiable contenant une liste. Les opérations sur la pile sont réalisées par les opérations classiques sur les listes appliquées à l'unique champ de l'enregistrement.

Fort de cette connaissance, rien ne nous interdit a priori d'accéder directement à la liste constituant la queue; cela n'est en fait pas le cas.

# let liste = queue.c ;;
Characters 13-18:
This expression has type int Stack.t but is here used with type 'a vm
Le compilateur proteste comme si il ne connaissait pas l'identificateur du champ du type Stack.t. C'est en fait le cas, comme nous nous en appercevons en regardant l'interface du module Stack.

camltabloadPART3/PMstack.mli[ocaml-2.02/stdlib/stack.mli]

Outre des commentaires expliquant comment utiliser les fonctions du module Stack, ce fichier explicite quels sont les valeurs, les types et les exceptions définies dans le fichier stack.ml. Plus précisément, l'interface fournit les noms des valeurs et leur type. En particulier, le nom du type t est donné, mais son implantation (c'est à dire l'identificateur du champ : c) n'est pas fournie. Ceci explique que l'utilisateur du module Stack ne puisse accéder directement à ce champ. Le type Stack.t est dit abstrait.

Les fonctions de manipulation des piles sont elles aussi simplement déclarées sans être définies. Il faut seulement, pour que le mécanisme d'inférence de type puisse juger de leur utilisation légitime, indiquer le type de chacune d'elle. Ceci est fait grâce à un nouveau mot clé :

Syntaxe


val   ident   :   un_type


Relation entre interface et implantation

Ainsi le module Stack est en fait constitué de deux entités : une interface et une implantation. Pour construire un module, il faut que tous les éléments déclarés dans l'interface soient effectivement définis dans l'implantation. Il est également nécessaire que les définitions des fonctions satisfassent la déclaration de type donnée dans l'interface3.

La relation entre interface et implantation n'est pas symétrique. L'implantation peut trés bien contenir plus de définitions que n'en demande l'interface. Typiquement, la définition d'une fonction un peu complexe pourra utiliser des fonctions auxiliaires dont le nom n'apparaît pas dans l'interface. Dans ce cas, le programmeur utilisant un tel module, tout comme il ne peut faire référence au champs c de la structure Stack.t, ne peut faire appel à la fonction auxiliaire non déclarée dans l'interface. De même il est possible dans l'interface d'apporter des restrictions sur le type des valeurs. Imaginons qu'un module définisse la fonction d'identité (let id x = x) mais que son interface déclare la valeur id de type int -> nt; alors les modules utilisant la valeur id ne pourront le faire que sur des entiers.

Le fichier d'interface, portant l'extension .mli, doit être compilé en utilisant la commande ocamlc -c avant la compilation des modules qui en dépendent dont le fichier d'implantation de ce même module. En cas d'absence d'un fichier d'interface, Objective CAML considère que la totalité du module est exportée; c'est à dire que toutes les déclarations de l'implantation sont présentes dans l'interface implicite avec leur type le plus général.

La dualité de constitution d'un module permet de rendre indépendant la déclaration des entités composant un module de leur implantation. Ainsi, on peut substituer au fichier d'implantation stack.ml de la distribution, un autre fichier, que l'on appellera toujours stack.ml et qui contient une implantation différente des piles reposant, par exemple, sur des tableaux :


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 = Array.length s.c
let iter f s = Array.iter f s.c


Cet ensemble de fonctions satisfait le requisit du fichier d'interface stack.mli. Le nouveau couple de fichiers stack.mli et stack.ml (nouvelle mouture) fournit une alternative au module Stack de la distribution d'Objective CAML.

On peut intégrer ce nouveau module en recompilant le fichier stack.ml et en l'intégrant à la bibliothèque standard (voir chapitre ).

Cette façon de procéder par fichier d'interface et fichier d'implantation pour constituer les modules permet la compilation séparée, mais les possibilités de structuration logique qu'elle offre sont faibles. En particulier, le nom des modules est implicite. Il est déduit du nom des fichiers constituant le module. L'amalgame entre module et fichier empêche d'avoir en même temps deux implantations d'une même interface, voire deux interface pour une même implantation. Il ne permet pas une organisation hiérarchique de modules, sous-modules, etc. Pour palier ce défaut, le mécanisme de module a été intégré à la syntaxe du langage de programmation. C'est ce que nous présentons dans la suite de ce chapitre.


Précédent Index Suivant