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.