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.