Langage des modules simples
Le langage Objective CAML possède un sous-langage de modules qui vient
s'ajouter au noyau du langage. Dans ce cadre, l'interface d'un module est
appelée sa signature et son implantation est appelée
structure. Lorsqu'il n'y a pas d'ambiguïté, nous
utiliserons plutôt le terme << module >> pour désigner la structure.
La syntaxe de déclaration des signatures et des structures est la
suivante :
Syntaxe
module type NOM = |
sig |
déclarations de l'interface |
end |
Syntaxe
module Nom = |
struct |
définition de l'implantation |
end |
Warning
Le nom d'un module doit impérativement commencer par une
majuscule. Celui d'une signature est libre, mais par convention
on utilise des noms en majuscules.
On peut également utiliser les signatures ou des structures
anonymes. On écrit alors simplement :
Syntaxe
sig déclarations end
Syntaxe
struct définitions end
Nous utiliserons les expressions signature et structure
pour désigner soit des noms de signature et de structure, soit leur
expression anonyme.
Toute structure a par défaut une signature calculée par l'inférence de
types qui reprend l'intégralité des définitions contenues dans la
structure. On peut lors de la définition d'une structure, préciser
quelle est la signature attendue en rajoutant une contrainte selon
l'une des deux syntaxes suivantes :
Syntaxe
module Nom : signature =
structure
Syntaxe
module Nom =
(structure : signature)
Lorsqu'une signature attendue est précisée, le système
vérifie que tout ce qui est déclaré dans la signature est défini dans
la structure Nom et que les types sont cohérents. En d'autres
termes, la signature attendue est incluse dans la signature par
défaut. Si tel est le cas, Nom devient un module de signature
signature et, de façon analogue à ce qui se passait avec les
fichiers d'interface, seules les déclarations apparaissant dans
la signature sont
accessibles à l'utilisateur du module.
L'accès aux entités déclarées d'un module se fait en utilisant
la notation pointée :
Syntaxe
Nom1.nom2
On dit alors que le nom nom2 est qualifié.
On peut rendre implicite le nom du module en utilisant la directive
d'ouverture des modules :
Syntaxe
open Nom
Dès lors, on peut utiliser les noms des entités sans les
qualifier. L'ouverture d'un module provoque, en cas d'identité de
nom, le masquage des entités préalablement définies, à
la façon des redéfinitions d'identificateurs.
Deux modules pour les piles
Reprenons les piles en utilisant le langage des modules. Nous
commençons par définir la signature d'une pile en reprenant les
déclarations du fichier stack.mli
:
# module
type
STACK
=
sig
type
'a
t
exception
Empty
val
create:
unit
->
'a
t
val
push:
'a
->
'a
t
->
unit
val
pop:
'a
t
->
'a
val
clear
:
'a
t
->
unit
val
length:
'a
t
->
int
val
iter:
('a
->
unit)
->
'a
t
->
unit
end
;;
module type STACK =
sig
type 'a t
exception Empty
val create : unit -> 'a t
val push : 'a -> 'a t -> unit
val pop : 'a t -> 'a
val clear : 'a t -> unit
val length : 'a t -> int
val iter : ('a -> unit) -> 'a t -> unit
end
On obtient une première implantation des piles en utilisant le module de
la bibliothèque standard :
# module
Stack_distrib
=
Stack
;;
module Stack_distrib :
sig
type 'a t = 'a Stack.t
exception Empty
val create : unit -> 'a t
val push : 'a -> 'a t -> unit
val pop : 'a t -> 'a
val clear : 'a t -> unit
val length : 'a t -> int
val iter : ('a -> unit) -> 'a t -> unit
end
On en définit une seconde utilisant des tableaux :
# module
Stack_perso
=
struct
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
=
s.
sp
let
iter
f
s
=
for
i=
0
to
pred
s.
sp
do
f
s.
c.
(i)
done
end
;;
module Stack_perso :
sig
type 'a t = { mutable sp: int; mutable c: 'a array }
exception Empty
val create : unit -> 'a t
val clear : 'a t -> unit
val size : int
val increase : 'a t -> unit
val push : 'a -> 'a t -> unit
val pop : 'a t -> 'a
val length : 'a t -> int
val iter : ('a -> 'b) -> 'a t -> unit
end
Les deux modules utilisent un type concret différent pour
implanter le type t.
# Stack_distrib.create
()
;;
- : '_a Stack_distrib.t = <abstr>
# Stack_perso.create
()
;;
- : '_a Stack_perso.t = {Stack_perso.sp=0; Stack_perso.c=[||]}
On retrouve l'abstraction de type en forçant la signature
du second module.
# module
Stack_perso
=
(Stack_perso
:
STACK)
;;
module Stack_perso : STACK
# Stack_perso.create()
;;
- : '_a Stack_perso.t = <abstr>
Les deux modules Stack_perso et Stack_distrib
n'ont en commun que le nom des fonctions qu'ils implantent. Par
contre, leurs types sont différents; il n'est donc pas possible
d'utiliser les fonctions de l'un pour manipuler les valeurs de l'autre :
# let
s
=
Stack_distrib.create()
;;
val s : '_a Stack_distrib.t = <abstr>
# Stack_perso.push
0
s
;;
Characters 19-20:
This expression has type 'a Stack_distrib.t = 'a Stack.t
but is here used with type int Stack_perso.t
Même si les deux modules avaient possédé un type t de
même implantation, le fait d'abstraire ce type en contraignant le
module avec la signature STACK interdit la possibilité de
partager les valeurs entre les deux modules.
# module
S1
=
(
Stack_perso
:
STACK
)
;;
module S1 : STACK
# module
S2
=
(
Stack_perso
:
STACK
)
;;
module S2 : STACK
# let
s
=
S1.create
()
;;
val s : '_a S1.t = <abstr>
# S2.push
0
s
;;
Characters 10-11:
This expression has type 'a S1.t but is here used with type int S2.t
Objective CAML ne dispose pour vérifier la compatibilité des types que
de leur nom (leur implantation étant abstraite) et ici ils sont
différents : S1.t et S2.t. C'est précisément
cette restriction qui permet l'abstraction de type en interdisant
l'accès à la définition des types dont on veut masquer
l'implantation.
Modules et portée lexicale
Nous donnons dans ce paragraphe deux exemples d'utilisation de signatures
pour masquer certaines déclarations.
Masquage de types
Abstraire un type permet de restreindre ses valeurs à celles qu'il
est possible de construire avec les fonctions que déclare la
signature du module où ce type est définit. Dans l'exemple
suivant, nous obtenons des entiers dont la construction nous assure
qu'ils sont obligatoirement différents de 0.
# module
Int_Star
=
(
struct
type
t
=
int
exception
Isnul
let
of_int
=
function
0
->
raise
Isnul
|
n
->
n
let
mult
=
(+
)
end
:
sig
type
t
exception
Isnul
val
of_int
:
int
->
t
val
mult
:
t
->
t
->
t
end
)
;;
module Int_Star :
sig type t exception Isnul val of_int : int -> t val mult : t -> t -> t end
Masquage de valeurs
Le masquage d'une valeur permet de réaliser un générateur de
symboles analogue à celui vu page ??.
On définit la signature GENSYM contenant seulement deux
déclarations de fonctions pour la génération de symboles.
# module
type
GENSYM
=
sig
val
reset
:
unit
->
unit
val
next
:
string
->
string
end
;;
On implante ensuite une structure cohérente pour une telle signature :
# module
Gensym
:
GENSYM
=
struct
let
c
=
ref
0
let
reset
()
=
c:=
0
let
next
s
=
incr
c
;
s
^
(string_of_int
!
c)
end;;
module Gensym : GENSYM
La référence c de la structure Gensym n'est pas
accessible en dehors des deux fonctions exportées.
# Gensym.reset();;
- : unit = ()
# Gensym.next
"T"
;;
- : string = "T1"
# Gensym.next
"X"
;;
- : string = "X2"
# Gensym.reset();;
- : unit = ()
# Gensym.next
"U"
;;
- : string = "U1"
# Gensym.c;;
Characters 0-8:
Unbound value Gensym.c
La déclaration de c peut être considérée comme locale à la
structure module Gensym puisqu'elle est masquée par la
signature associée au module. La contrainte de signature nous a permis
de reproduire plus simplement la définition des fonctions
reset_s et new_s qui utilisaient une déclaration
locale (voir page ??).
Différentes vues d'un même module
Le langage de module avec contraintes de signature permet d'offrir
plusieurs vues d'une même structure. On pourra, par exemple avoir un
<< super-utilisateur >> du module Gensym qui est capable de
remettre à jour le compteur et un utilisateur ordinaire qui ne peut
que créer un nouveau symbole sans maîtriser le compteur. Pour
obtenir ce dernier, il suffit de poser la signature :
# module
type
USER_GENSYM
=
sig
val
next
:
string
->
string
end;;
On crée ensuite le module correspondant par la déclaration :
# module
UserGensym
=
(Gensym
:
USER_GENSYM)
;;
module UserGensym : USER_GENSYM
# UserGensym.next
"U"
;;
- : string = "U2"
# UserGensym.reset()
;;
Characters 0-16:
Unbound value UserGensym.reset
Pour réaliser ce nouveau module on a réutilisé
le module Gensym. De plus, les deux modules partagent le
même compteur :
# Gensym.next
"U"
;;
- : string = "U3"
# Gensym.reset()
;;
- : unit = ()
# UserGensym.next
"V"
;;
- : string = "V1"
Partage de types entre modules
L'incompatibilité entre types abstraits signalée un peu avant
(page ??) pose problème lorsque l'on désire
partager un type abstrait entre plusieurs modules. Nous
examinons deux façons de procéder au partage. L'une est
une construction explicite du langage de modules, l'autre utilise la
structure de bloc lexical des modules.
Partage par contrainte
Illustrons le problème à l'aide du petit exemple suivant. On
définit un module M qui fournit un type abstrait
M.t. Nous le restreignons ensuite selon deux signatures
différentes n'autorisant pas les mêmes opérations.
# module
M
=
(
struct
type
t
=
int
ref
let
create()
=
ref
0
let
add
x
=
incr
x
let
get
x
=
if
!
x>
0
then
(decr
x;
1
)
else
failwith
"Empty"
end
:
sig
type
t
val
create
:
unit
->
t
val
add
:
t
->
unit
val
get
:
t
->
int
end
)
;;
# module
type
S1
=
sig
type
t
val
create
:
unit
->
t
val
add
:
t
->
unit
end
;;
# module
type
S2
=
sig
type
t
val
get
:
t
->
int
end
;;
# module
M1
=
(M:
S1)
;;
module M1 : S1
# module
M2
=
(M:
S2)
;;
module M2 : S2
Pour obtenir l'identification désirée des types M1.t et
M2.t, Objective CAML dispose d'une syntaxe pour contraindre un type
normalement abstrait dans une signature.
Syntaxe
NOM with
type t1 = t2
and ...
Il s'agit d'une contrainte de type forçant le type t1
déclaré par la signature NOM à être égal au type
t2.
On peut poser des contraintes globales sur tous les types d'un module en
utilisant la contrainte :
Syntaxe
with module Nom1 = Nom2
En utilisant de telles contraintes de partage, on peut déclarer les deux
modules M1 et M2 comme manipulant la même
structure de données.
# module
M1
=
(M:
S1
with
type
t
=
M.t)
;;
module M1 : sig type t = M.t val create : unit -> t val add : t -> unit end
# module
M2
=
(M:
S2
with
type
t
=
M.t)
;;
module M2 : sig type t = M.t val get : t -> int end
# let
x
=
M1.create()
in
M1.add
x
;
M2.get
x
;;
- : int = 1
Partage et sous-modules
Une autre solution pour assurer le partage de type est d'utiliser le
mécanisme des sous-modules. En définissant deux sous-modules
(M1 et M2) partageant un type de données abstrait d'un
module englobant M, nous pouvons parvenir au résultat
souhaité.
# module
M
=
(
struct
type
t
=
int
ref
module
M_hide
=
struct
let
create()
=
ref
0
let
add
x
=
incr
x
let
get
x
=
if
!
x>
0
then
(decr
x;
1
)
else
failwith"Empty"
end
module
M1
=
M_hide
module
M2
=
M_hide
end
:
sig
type
t
module
M1
:
sig
val
create
:
unit
->
t
val
add
:
t
->
unit
end
module
M2
:
sig
val
get
:
t
->
int
end
end
)
;;
module M :
sig
type t
module M1 : sig val create : unit -> t val add : t -> unit end
module M2 : sig val get : t -> int end
end
On obtient bien le résultat voulu et une valeur créée par
M1 peut être manipulée par M2 :
# let
x
=
M.
M1.create()
;;
val x : M.t = <abstr>
# M.
M1.add
x
;
M.
M2.get
x
;;
- : int = 1
On rajoute cependant un peu de lourdeur par rapport à la solution
précédente : l'accès aux fonctions de M1 et M2
se fait via le module englobant M.
Modules simples et extension
Un module est une entité définie une fois pour toutes. En
particulier, lorsque nous définissons un type abstrait à l'aide du
mécanisme de modules nous ne pouvons plus en étendre les
traitements. En particulier, s'il n'a pas été défini de
fonction de création, on ne pourra jamais obtenir de valeur de ce
type !
Une façon brutale d'augmenter les traitements fournis par un
module est d'éditer les sources et de rajouter ce que l'on désire
dans la signature et la structure. Mais alors, on n'a plus du tout
affaire au même module et toutes les applications qui utilisaient la
version originale du module sont à recompiler. Notons cependant que
si la redéfinition des composants du module n'a pas modifié les
éléments de l'interface originale, il suffit uniquement de
recompiler l'ensemble de l'application sans avoir à modifier ce qui
avait été écrit.