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 8. Nous allons voir, dans ce paragraphe, comment réaliser de tels modules.

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.04/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 apercevons 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 s'il ne connaissait pas l'identificateur du champ du type Stack.t. C'est en fait le cas, comme nous nous en apercevons en regardant l'interface du module Stack.

ocaml-2.04/stdlib/stack.mli
(* Module [Stack]: last-in first-out stacks *)
(* This module implements stacks (LIFOs), with in-place modification. *)

type 'a t (* The type of stacks containing elements of type ['a]. *)

exception Empty (* Raised when [pop] is applied to an empty stack. *)

val create: unit -> 'a t
(* Return a new stack, initially empty. *)
val push: 'a -> 'a t -> unit
(* [push x s] adds the element [x] at the top of stack [s]. *)
val pop: 'a t -> 'a
(* [pop s] removes and returns the topmost element in stack [s],
or raises [Empty] if the stack is empty. *)
val clear : 'a t -> unit
(* Discard all elements from a stack. *)
val length: 'a t -> int
(* Return the number of elements in a stack. *)
val iter: ('a -> unit) -> 'a t -> unit
(* [iter f s] applies [f] in turn to all elements of [s],
from the element at the top of the stack to the element at the
bottom of the stack. The stack itself is unchanged. *)


Outre des commentaires expliquant comment utiliser les fonctions du module Stack, ce fichier explicite quels sont les valeurs, les types et les exceptions définis 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 types puisse juger de leur utilisation légitime, indiquer le type de chacune d'elles. Ceci est fait grâce à un nouveau mot clé :

Syntaxe


val nom : 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'interface1.

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 champ 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.

La dualité de constitution d'un module permet de rendre la déclaration des entités composant un module indépendante de leur implantation. Ainsi, on peut substituer au fichier d'implantation stack.ml de la distribution, un autre fichier, que l'on appelle 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.

Compilation séparée

À l'instar des autres langages de programmation modernes, Objective CAML permet de découper un programme en plusieurs unités de compilation. Une unité de compilation est constituée de deux fichiers, le fichier d'implantation (d'extension .ml) et le fichier d'interface (d'extension .mli). Chaque unité de compilation est vue comme un module. Ainsi la compilation du fichier nom.ml produit le module Nom2. Les valeurs, types et exceptions définis dans un module peuvent être référencés soit en utilisant la notation pointée (Module.identificateur), soit en utilisant le mot clé open.

      nom1.ml        nom2.ml
type t = { x:int ; y:int } ;; let val = { Nom1.x = 1 ; Nom1.y = 2 } ;;
let f c = c.x + c.y ;; Nom1.f val ;;
open Nom1 ;;
f val ;;

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 en particulier 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.

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

Pour engendrer 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 
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 interfaces pour une même implantation. Pour pallier 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