Bibliothèque standard
La bibliothèque standard regroupe un ensemble de modules
stables. Ceux-ci sont indépendants des systèmes d'exploitation. Il y
a actuellement 29 modules dans la bibliothèque standard contenant 400
fonctions, 30 types dont la moitié sont abstraits, 8 exceptions, 10
sous-modules et 3 modules paramétrés. Il est clair que nous n'allons
pas détailler l'ensemble des déclarations de tous ces modules. En
effet le manuel de référence [LRVD99] le fait déjà très
bien. Seuls les modules présentant un nouveau concept ou une réelle
difficulté de manipulation seront détaillés.
La bibliothèque standard peut être découpée en quatre grandes parties :
-
structures linéaires de données (15 modules), dont certains
sont déjà apparus dans la première partie ;
- entrées-sorties (4 modules), pour le formatage des sorties,
la persistance et la création de clés pour la cryptographie ;
- analyses lexicale et syntaxique (4 modules). Leur
description est reportée au chapitre 11 (page
??) ;
- interface avec le système qui permet de communiquer et
d'analyser les paramètres passés à une commande, de naviguer dans les
catalogues et d'accéder aux fichiers.
À ces quatre ensembles, on ajoute un cinquième contenant quelques
utilitaires de manipulation ou de création de données comme des
fonctions de traitement de caractères ou un générateur de
nombres pseudo-aléatoires, etc.
Utilitaires
Les modules que nous avons baptisés << utilitaires >> concernent :
-
les caractères : module Char qui contient
essentiellement des conversions ;
- copie d'objets : Oo qui sera présenté au chapitre
15 (page ??), sur la programmation
objet ;
- évaluation retardée : Lazy présenté en première
partie, page ?? ;
- générateur de nombres aléatoires : Random que nous
décrivons ci-dessous.
Génération de nombres aléatoires
Le module Random est un générateur de nombres
pseudo-aléatoires. Il implante une fonction de génération de nombres
aléatoires à partir d'un nombre ou d'une suite de nombres appelé
racine. Pour que cette fonction ne retourne pas toujours la même
suite de nombres, le programmeur doit lui passer une racine différente
à chaque initialisation. À partir de cette racine la fonction
engendre une suite de nombres non prévisible. Néanmoins une
initialisation avec la même racine créera la même suite. Il faut donc
trouver une ressource extérieure au programme, comme la date du jour
en millisecondes, ou le temps passé depuis le début du programme,
pour initialiser correctement le générateur.
Voici les
fonctions du module :
-
initialisation : init de type int -> unit et
full_init de type int array -> unit qui
initialisent le générateur. La deuxième fonction prend un tableau de
racines.
- demande d'un nombre : bits de type unit ->
int retourne un entier positif, int de type int ->
int retourne un entier positif compris entre 0 et une borne passée
en tant que paramètre, et float qui retourne un flottant en
lui passant une borne.
Structures de données linéaires
Les modules sur les structures de données linéaires sont :
-
modules simples : Array, String,
List, Sort, Stack, Queue,
Buffer, Hashtbl (qui est aussi paramétré) et Weak ;
- modules paramétrés : Hashtbl (de paramètre
HashedType), Map et Set (de paramètre
OrderedType).
Les modules paramétrés sont construits à partir d'autres modules
accroissant ainsi leur généricité. La construction de modules
paramétrés sera présentée au chapitre 14, page
??.
Structures linéaires de données simples
Le nom du module décrit le type de structures de données manipulées par
le module. Si le type est abstrait, c'est-à-dire si sa représentation
est masquée, la convention actuelle est de le nommer t à
l'intérieur du module. Ces modules implantent les structures suivantes :
-
module Array : vecteurs
- module List : listes
- module String : chaînes de caractères
- module Hashtbl : tables de hachage (type abstrait)
- module Buffer : chaînes de caractères
extensibles (type abstrait)
- module Stack : piles (type abstrait)
- module Queue : files d'attente ou FIFO (type
abstrait)
- module Weak : vecteur de pointeurs faibles
(type abstrait)
Signalons un dernier module manipulant des structures linéaires :
-
module Sort : tri sur les listes et les vecteurs, fusion
de listes
Famille de fonctions communes
Tous ces modules, à l'exception du module Sort, définissent
une structure de données, les fonctions de création et d'accès aux
éléments de cette structures ainsi que des fonctions de manipulation
incluant des conversions vers d'autres types. Seul le module
List est sans modification physique. Nous ne donnerons pas la
description complète de toutes ces fonctions. Néanmoins, nous allons
nous intéresser aux familles de fonctions que l'on rencontre dans ces
modules, puis nous détaillerons les modules List et
Array qui sont les structures les plus communes en
programmation fonctionnelle et impérative.
On retrouve peu ou prou les fonctionnalités suivantes dans tous les
modules :
-
une fonction length qui prend une valeur du type
et calcule un entier correspondant à sa longueur ;
- une fonction clear remettant la structure linéaire à
vide, si elle est modifiable ;
- une fonction d'ajout d'un élément, add en général, mais
pouvant être dénommée différemment selon les habitudes (comme par
exemple push pour les piles) ;
- une fonction d'accès au i-ème élément, souvent nommée
get ;
- une fonction de retrait d'un élément (souvent le premier)
remove ou take.
De même certaines conventions sur les fonctions de parcours et de
traitement se retrouvent dans un certain nombre de modules :
-
map : qui applique une fonction sur tous les éléments
de la structure et retourne une nouvelle structure contenant les
résultats de ces appels ;
- iter : proche de map mais ne tient pas compte
des résultats et retourne ().
Pour les structures dont les éléments sont indicés. on a :
-
fill : qui remplace (modifie en place) une partie de la
structure par une valeur ;
- blit : qui copie une partie d'une structure dans une
autre structure du même type ;
- sub : qui copie une partie d'une structure et en crée
une nouvelle.
Modules List et Array
Nous décrivons les fonctions de ces deux bibliothèques en mettant
l'accent sur les similitudes et les particularités de chacune
d'elles. Pour les fonctions communes aux deux modules, t
désigne le type 'a list ou 'a array. Lorsqu'une
fonction est propre à un module, nous utiliserons la notation
pointée.
Fonctionnalités communes ou analogues
La première d'entre elles est le calcul de la longueur.
Deux fonctions permettent de concaténer deux structures ou toutes
les structures d'une liste.
append |
: |
'a t -> 'a t -> 'a t |
concat |
: |
'a t list -> 'a t |
Chacun des deux modules possède une fonction d'accès à un
élément désigné par sa position dans la structure.
List.nth |
: |
'a list -> int -> 'a |
Array.get |
: |
'a array -> int -> 'a |
La fonction d'accès à un élément d'indice i d'un
vecteur t, dont l'usage est fréquent, possède un raccourci
syntaxique : t.(i).
Deux fonctions permettent d'appliquer un traitement (une fonction) à
tous les éléments d'une structure.
iter |
: |
('a -> unit) -> 'a t -> unit |
map |
: |
('a -> 'b) -> 'a t -> 'b t |
On peut utiliser iter pour afficher le contenu d'une liste
ou d'un vecteur.
# let
print_content
iter
print_item
xs
=
iter
(fun
x
->
print_string"("
;
print_item
x;
print_string")"
)
xs;
print_newline()
;;
val print_content : (('a -> unit) -> 'b -> 'c) -> ('a -> 'd) -> 'b -> unit =
<fun>
# print_content
List.iter
print_int
[
1
;2
;3
;4
;5
]
;;
(1)(2)(3)(4)(5)
- : unit = ()
# print_content
Array.iter
print_int
[|
1
;2
;3
;4
;5
|]
;;
(1)(2)(3)(4)(5)
- : unit = ()
La fonction map construit une nouvelle structure contenant le
résultat de l'application. On peut le voir par exemple sur les
vecteurs dont le contenu est modifiable :
# let
a
=
[|
1
;2
;3
;4
|]
;;
val a : int array = [|1; 2; 3; 4|]
# let
b
=
Array.map
succ
a
;;
val b : int array = [|2; 3; 4; 5|]
# a,
b;;
- : int array * int array = [|1; 2; 3; 4|], [|2; 3; 4; 5|]
Deux itérateurs permettent de composer l'application partielle d'une
fonction à chacun des éléments d'une structure.
fold_left |
: |
('a -> 'b -> 'a) -> 'a -> 'b t -> 'a |
fold_right |
: |
('a -> 'b -> 'b) -> 'a t -> 'b -> 'b |
Il faut fournir à ces itérateurs un cas de base qui donne la
valeur par défaut lorsque la structure est vide.
fold_left f r [ v1; v2; ... ; vn] |
= |
f ... ( f (f r v1) v2 ) ... vn |
fold_right f [ v1; v2; ... ; vn] r |
= |
f v1 ( f v2 ... (f vn r) ... ) |
Ces fonctions permettent de transformer facilement des opérations
binaires en opérations n-aires. Lorsque l'opération est
commutative et associative, l'itération à gauche ou à droite est
indifférente :
# List.fold_left
(+
)
0
[
1
;2
;3
;4
]
;;
- : int = 10
# List.fold_right
(+
)
[
1
;2
;3
;4
]
0
;;
- : int = 10
# List.fold_left
List.append
[
0
]
[[
1
]
;[
2
]
;[
3
]
;[
4
]]
;;
- : int list = [0; 1; 2; 3; 4]
# List.fold_right
List.append
[[
1
]
;[
2
]
;[
3
]
;[
4
]]
[
0
]
;;
- : int list = [1; 2; 3; 4; 0]
Remarquons que la liste vide est élément neutre à gauche et à
droite pour la concaténation binaire. On retrouve donc, pour ce cas
particulier, l'équivalence des deux expressions :
# List.fold_left
List.append
[]
[[
1
]
;[
2
]
;[
3
]
;[
4
]]
;;
- : int list = [1; 2; 3; 4]
# List.fold_right
List.append
[[
1
]
;[
2
]
;[
3
]
;[
4
]]
[]
;;
- : int list = [1; 2; 3; 4]
On a, en fait, retrouvé la fonction List.concat.
Manipulations propres aux listes
Il est d'usage d'avoir sur les listes les fonctions suivantes que
fournit le module List :
List.hd |
: |
'a list -> 'a |
|
|
premier élément de la liste |
List.tl |
: |
'a list -> 'a |
|
|
liste privée de son premier élément |
List.rev |
: |
'a list -> 'a list |
|
|
retournement d'une liste |
List.mem |
: |
'a -> 'a list -> bool |
|
|
test d'appartenance |
List.flatten |
: |
'a list list -> 'a list |
|
|
aplatissement d'une liste de listes |
List.rev_append |
: |
'a list -> 'a list -> 'a list |
|
|
est égale à append (rev l1) l2 |
Les deux premières fonctions sont partielles. Elles ne sont pas
définies sur la liste vide et déclenchent une exception
Failure. Il existe une variante de mem : memq
qui utilise l'égalité physique.
# let
c
=
(1
,
2
)
;;
val c : int * int = 1, 2
# let
l
=
[
c]
;;
val l : (int * int) list = [1, 2]
# List.memq
(1
,
2
)
l
;;
- : bool = false
# List.memq
c
l
;;
- : bool = true
Le module List fournit deux itérateurs particuliers
généralisant la conjonction et la disjonction booléennes :
List.for_all et List.exists que l'on définit par
itération :
# let
for_all
f
xs
=
List.fold_right
(fun
x
->
fun
b
->
(f
x)
&
b)
xs
true
;;
val for_all : ('a -> bool) -> 'a list -> bool = <fun>
# let
exists
f
xs
=
List.fold_right
(fun
x
->
fun
b
->
(f
x)
or
b)
xs
false
;;
val exists : ('a -> bool) -> 'a list -> bool = <fun>
Il existe des variantes de tous les itérateurs du module List
prenant en argument deux listes et les parcourant en
parallèle (iter2, map2, etc.). Si elles ne sont
pas de même longueur, l'exception Invalid_argument est
déclenchée.
La recherche d'éléments d'une liste selon un critère donné sous
forme de fonction booléenne peut se faire en utilisant les fonctions
suivantes :
List.find |
: |
('a -> bool) -> 'a list -> 'a |
List.find_all |
: |
('a -> bool) -> 'a list -> 'a
list |
La fonction find_all a un alias : filter.
Une variante de la fonction générale de recherche est la fonction
de partitionnement d'une liste :
List.partition |
: |
('a -> bool) -> 'a list -> 'a list * 'a list |
Comme utilitaires souvent nécessaires, on trouve dans le module
List deux fonctions permettant de diviser ou de créer des
listes de couples :
List.split |
: |
('a * 'b) list -> 'a list * 'b list |
List.combine |
: |
'a list -> 'b list -> ('a * 'b) list |
Enfin, une structure combinant listes et couples est souvent
utilisée : les listes d'associations. Elles servent à
stocker des valeurs associées à des clés. Ce sont des listes de
couples dont la première composante joue le rôle de clé et la
seconde, celui d'information associée. On a pour traiter de telles
données :
List.assoc |
: |
'a -> ('a * 'b) list -> 'b |
|
|
extrait l'information associée à une clé |
List.mem_assoc |
: |
'a -> ('a * 'b) list -> bool |
|
|
teste l'existence d'une clé |
List.remove_assoc |
: |
'a -> ('a * 'b) list -> ('a
* 'b) list |
|
|
suppression d'un élément correspondant à une clé |
Ces fonctions ont chacune une variante utilisant l'égalité
physique au lieu de l'égalité structurelle: List.assq,
List.mem_assq et List.remove_assq.
Manipulations propres aux vecteurs
Les vecteurs que l'on utilise souvent en programmation impérative
sont des structures physiquement modifiables. Le module Array
fournit une fonction de modification de la valeur d'un élément :
Array.set |
: |
'a array -> int -> 'a -> unit |
Comme get, la fonction set possède un raccourci
syntaxique : t.(i) <- a.
Il existe trois fonctions d'allocation de vecteurs :
Array.create |
: |
int -> 'a -> 'a array |
|
|
crée un vecteur d'une taille donnée
dont tous les éléments sont initialisés avec une même
valeur |
Array.make |
: |
int -> 'a -> 'a array |
|
|
alias de create |
Array.init |
: |
int -> (int -> 'a) -> 'a array |
|
|
crée un vecteur d'une taille donnée
dont chaque élément est initialisé avec le résultat de l'application
d'une fonction à l'indice de l'élément initialisé |
Comme leur usage est courant, le module Array fournit deux
fonctions de création de matrices (vecteurs de vecteurs) :
Array.create_matrix |
: |
int -> int -> 'a -> 'a array array |
Array.make_matix |
: |
int -> int -> 'a -> 'a array
array |
La fonction set se généralise en une fonction modifiant
les valeurs d'un intervalle décrit par un indice de départ et une
longueur :
Array.fill |
: |
'a array -> int -> int -> 'a -> unit |
On peut copier l'intégralité d'un vecteur ou extraire un sous
vecteur (décrit par un indice de début et une longueur) pour
obtenir une nouvelle structure :
Array.copy |
: |
'a array -> 'a array |
Array.sub |
: |
'a array -> int -> int -> 'a array |
La copie ou l'extraction peuvent aussi se faire vers un autre vecteur :
Array.blit |
: |
'a array -> int -> 'a array -> int -> int -> unit |
Le premier argument entier est l'indice dans le premier vecteur, le
deuxième, l'indice dans le second vecteur et le troisième argument
entier, le nombre de valeurs copiées. Les trois fonctions
blit, sub et fill déclenchent l'exception
Invalid_argument.
L'usage privilégié des indices dans les fonctions de manipulation
de vecteurs a conduit à la définition de deux itérateurs
particuliers :
Array.iteri |
: |
(int -> 'a -> unit) -> 'a array -> unit |
Array.mapi |
: |
(int -> 'a -> 'b) -> 'a array -> 'b array |
Elles appliquent une fonction dont le premier argument est l'indice de
la case traitée.
#
let
f
i
a
=
(string_of_int
i)
^
":"
^
(string_of_int
a)
in
Array.mapi
f
[|
4
;
3
;
2
;
1
;
0
|]
;;
- : string array = [|"0:4"; "1:3"; "2:2"; "3:1"; "4:0"|]
Le module Array ne fournit pas de fonction permettant de
modifier en place le contenu des cases d'un vecteur avec le résultat
d'un calcul sur leur contenu, mais on peut facilement l'obtenir avec
iteri :
# let
iter_and_set
f
t
=
Array.iteri
(fun
i
->
fun
x
->
t.
(i)
<-
f
x)
t
;;
val iter_and_set : ('a -> 'a) -> 'a array -> unit = <fun>
# let
v
=
[|
0
;1
;2
;3
;4
|]
;;
val v : int array = [|0; 1; 2; 3; 4|]
# iter_and_set
succ
v
;;
- : unit = ()
# v
;;
- : int array = [|1; 2; 3; 4; 5|]
Enfin, le module Array fournit deux fonctions de conversion
avec les listes :
Array.of_list |
: |
'a list -> 'a array |
Array.to_list |
: |
'a array -> 'a list |
Entrées-sorties
La bibliothèque standard comporte les quatre modules d'entrées-sorties :
-
module Printf : pour le formatage en sortie;
- Format : pour un formatage en boîte gérant
automatiquement les sauts de ligne;
- module Marshal : implantant un mécanisme de valeurs
persistantes ;
- module Digest : pour créer des clés uniques.
La description du module Marshal sera donnée plus loin dans
ce chapitre lorsque nous aborderons le traitement des données
persistantes (voir page ??).
Module Printf
Le module Printf permet de formater l'affichage à la manière
de la fonction printf de la bibliothèque du langage C. Le
format d'affichage est représenté sous forme d'une chaîne de
caractères, qui sera décodée selon les conventions du printf
de C, c'est-à-dire en spécialisant le caractère %. C'est ce
caractère suivi d'une lettre qui indiquera le type de l'argument à
mettre à cet emplacement. Le format suivant "(x=%d, y=%d)"
indique qu'il faudra placer deux entiers à la place des %d dans
la chaîne de sortie.
Spécifications des formats
Un format définit des paramètres pour une chaîne
d'affichage. Ceux-ci, de type de base : int, float,
char et string, seront convertis en chaînes
et remplaceront leur occurrence dans la chaîne
d'affichage. Les valeurs 77 et 43 fournie au format
"(x=%d, y=%d)" engendreront la chaîne d'affichage
complète "(x=77, y=43)"
. Les principales lettres
indiquant le type de la conversion à effectuer sont données figure
8.1.
Type |
Lettre |
Résultat |
entier |
d ou i |
décimal signé |
|
u |
décimal non signé |
|
x |
hexadécimal non signé en minuscule |
|
X |
idem en majuscule |
caractère |
c |
caractère |
chaîne |
s |
chaîne |
flottant |
f |
décimal |
|
e ou E |
en notation avec exposant |
|
g ou G |
idem |
booléen |
b |
true ou false |
spécial |
a ou t |
paramètre fonctionnel |
|
|
de type (out_channel -> 'a -> unit) -> 'a -> unit |
|
|
ou out_channel -> unit |
Figure 8.1 :
Convention de conversion
Le format permet aussi d'assurer le cadrage d'une conversion, ce qui
permet l'alignement des valeurs à afficher. On peut principalement
indiquer la taille en caractères de la conversion. Pour cela on
intercalera entre le caractère % et le type de conversion un
nombre entier comme dans %10d qui indique une conversion
essayant de se cadrer à droite sur dix caractères. Si la taille de la
conversion dépasse cette limite, il ne sera pas tenu compte de cette
limite. Un nombre négatif effectuera un cadrage à gauche. Pour les
conversions de nombres flottants, il est utile de pouvoir spécifier la
précision d'affichage. On intercalera alors un point suivi d'un nombre
pour indiquer le nombre de caractères après la virgule comme dans
%.5f qui indique cinq caractères à droite du point décimal.
Il existe deux lettres de format particulières : a et
t qui indiquent un argument fonctionnel. Typiquement, une
fonction d'affichage définie par l'utilisateur. C'est une
spécificité d'Objective CAML.
Fonctions du module
Les types des cinq fonctions de ce module sont donnés figure
8.2.
fprintf |
: |
out_channel -> ('a, out_channel, unit) format -> 'a |
printf |
: |
('a, out_channel, unit) format -> 'a |
eprintf |
: |
('a, out_channel, unit) format -> 'a |
sprintf |
: |
('a, unit, string) format -> 'a |
bprintf |
: |
Buffer.t -> ('a, Buffer.t, string) format -> 'a |
Figure 8.2 :
Fonctions de formatage de
Printf
La fonction fprintf prend un canal, un format et des
arguments de type décrit dans le format. Les fonctions
printf et eprintf sont des versions spécialisées sur
la sortie standard et la sortie erreur. Enfin sprintf et
bprintf n'affichent pas le résultat de la conversion, mais
retournent la chaîne correspondante.
Voici quelques exemples simples d'utilisation des formats.
# Printf.printf
"(x=%d, y=%d)"
3
4
7
8
;;
(x=34, y=78)- : unit = ()
# Printf.printf
"nom = %s, age = %d"
"mapom"
1
8
;;
nom = mapom, age = 18- : unit = ()
# let
s
=
Printf.sprintf
"%10.5f\n%10.5f\n"
(-.
1
2
.
2
4
)
(2
.
3
0
0
0
0
0
0
8
)
;;
val s : string = " -12.24000\n 2.30000\n"
# print_string
s
;;
-12.24000
2.30000
- : unit = ()
L'exemple suivant construit une fonction d'affichage d'une matrice de
flottants sous un format donné.
# let
print_mat
m
=
Printf.printf
"\n"
;
for
i=
0
to
(Array.length
m)-
1
do
for
j=
0
to
(Array.length
m.
(0
))-
1
do
Printf.printf
"%10.3f"
m.
(i).
(j)
done
;
Printf.printf
"\n"
done
;;
val print_mat : float array array -> unit = <fun>
# print_mat
(Array.create
4
[|
1
.
2
;
-.
4
4
.
2
2
;
3
5
.
2
|]
)
;;
1.200 -44.220 35.200
1.200 -44.220 35.200
1.200 -44.220 35.200
1.200 -44.220 35.200
- : unit = ()
Remarque sur le type format
La description d'un format adopte la syntaxe des chaînes de
caractères, mais ce n'est pas une valeur de type string. Le
décodage d'un format, selon les conventions précédentes, construit une
valeur de type format où le paramètre 'a est
instancié soit à unit si le format ne mentionne pas de
paramètre, soit par un type fonctionnel correspondant à une fonction
pouvant recevoir autant d'arguments que mentionnés et retournant la
valeur de type unit.
On peut illustrer ce traitement en appliquant partiellement la fonction
printf à un format :
# let
p3
=
Printf.printf
"début\n%d est val1\n%s est val2\n%f est val3\n"
;;
début
val p3 : int -> string -> float -> unit = <fun>
On obtient ainsi une fonction qui attend trois arguments. Remarquez
que le mot début a déjà été affiché. Un autre
format aurait donné un autre type de fonction :
# let
p2
=
Printf.printf
"début\n%f est val1\n%s est val2\n"
;;
début
val p2 : float -> string -> unit = <fun>
En fournissant petit à petit ses arguments à p3, on
obtient les affichages au fur et à mesure :
# let
p31
=
p3
4
5
;;
45 est val1
val p31 : string -> float -> unit = <fun>
# let
p32
=
p31
"hello"
;;
hello est val2
val p32 : float -> unit = <fun>
# let
p33
=
p32
3
.
1
4
;;
3.140000 est val3
val p33 : unit = ()
# p33
;;
- : unit = ()
La dernière valeur obtenue n'affiche rien : c'est la valeur
() du type unit.
On ne peut pas construire un format en utilisant des valeurs de
type string :
# let
f
d
=
Printf.printf
(d^
d);;
Characters 27-30:
This expression has type string but is here used with type
('a, out_channel, unit) format
Le compilateur ne peut connaître la valeur de la chaîne passée
en argument. Il ne peut donc pas connaître le type qui sert à
instancier le paramètre 'a du type format.
D'autre part, les chaînes de caractères sont des valeurs
physiquement modifiables, il serait alors possible de modifier, par
exemple, la partie %d par une autre lettre, modifiant ainsi
dynamiquement un format d'affichage. C'est en contradiction avec la
génération statique de la fonction de conversion.
Module Digest
Une fonction de hachage convertit une chaîne de
caractères de taille quelconque en une chaîne de caractères de taille
fixe, le plus souvent plus petite. Ces fonctions de hachage retournent
une empreinte (digest) de leur entrée.
De telles fonctions sont utilisées pour la construction de tables de
hachage, comme dans le module Hashtbl, permettant de tester
rapidement si un élément appartient à une telle table grâce à un accès
direct à l'empreinte. Par exemple la fonction f_mod_n,
qui effectue la somme des codes ASCII des caractères d'une chaîne
modulo n, est une fonction de hachage. Si on crée un tableau de
dimension n pour ranger ces chaînes, de part l'empreinte on obtient un
accès direct. Néanmoins deux chaînes peuvent retourner la même
empreinte. Dans ces cas de collisions, on ajoute à la table de hachage
une extension pour stocker ces éléments. S'il y a trop de collisions,
alors l'accès à la table de hachage est peu efficace. Si l'empreinte
a une taille de n bits, alors la probabilité de collision entre deux
chaînes différentes est de 1/2n.
Une fonction de hachage à sens unique possède une probabilité
très faible de collision. Il est ainsi difficile, étant donnée
une empreinte, de construire une chaîne possédant cette
empreinte. La fonction précédente f_mod_n n'en est, à
l'évidence, pas une. Les fonctions de hachage à sens unique
permettent l'authentification d'une chaîne, que cela soit pour un texte
envoyé sur Internet, un fichier, etc.
Le module Digest utilise l'algorithme MD5, pour
Message Digest 5. Il retourne une empreinte sur 128 bits. Bien que
l'algorithme soit public, il est impossible (à ce jour) d'effectuer une
reconstruction à partir d'une empreinte. Ce module définit le type
Digest.t comme une abréviation du type string. La
figure 8.3 détaille les principales fonctions de ce
module.
string |
: |
string -> t |
|
|
retourne l'empreinte d'une chaîne |
file |
: |
string -> t |
|
|
retourne l'empreinte d'un fichier |
Figure 8.3 : Fonctions du module Digest
On utilise la fonction string dans l'exemple suivant sur une
petite chaîne et sur une grande construite à partir de la
première. L'empreinte est toujours de taille fixe.
# let
s
=
"Le petit chat est mort..."
;;
val s : string = "Le petit chat est mort..."
# Digest.string
s;;
- : Digest.t = "[\138\236]5\015\156'\243\232>\176IdF\022"
# let
r
=
ref
s
in
for
i=
1
to
1
0
0
do
r:=
s^
!
r
done;
Digest.string
!
r;;
- : Digest.t = "\148\022\201?j\141\159\143\168\241;\004\158\139\196\147"
La création d'une empreinte sur des programmes permet de garantir le
contenu de celui-ci évitant ainsi l'utilisation d'une mauvaise
version. Par exemple, lors du chargement dynamique de code (voir page
??), une empreinte est utilisée pour sélectionner
le fichier de code-octet à charger.
# Digest.file
"basic.ml"
;;
- : Digest.t = "\179\026\191\137\157Ly|^w7\183\164:\167q"
Persistance
La persistance est la conservation d'une valeur en dehors de
l'exécution courante d'un programme. C'est le cas quand on écrit une
valeur dans un fichier. Cette valeur est alors accessible à tout
programme ayant accès au fichier. L'écriture et la lecture d'une
valeur persistante réclament la définition d'un format de
représentation ou codage des données. En effet, il faut savoir
passer d'une structure complexe stockée en mémoire, telle un
arbre, à une structure linéaire, une suite d'octets,
stockée sur fichier. C'est pourquoi le codage des valeurs
persistantes s'appelle linéarisation1.
Réalisation et difficultés de la linéarisation
La mise en oeuvre d'un mécanisme de linéarisation des données
demande des choix et présente des difficultés que nous décrivons
ci-dessous.
-
lecture-écriture de données.
Comme la mémoire peut toujours être vue comme un vecteur de mots, une
valeur peut toujours correspondre à la mémoire qu'elle occupe, quitte
à ne conserver que la partie utile en compactant alors la valeur.
- partage ou copie.
L'aplatissement d'une donnée doit-il conserver le partage ?
Typiquement un arbre binaire qui possède deux fils identiques (au sens
de l'égalité physique) peut indiquer, pour le deuxième fils, qu'il a
déjà sauvegardé le premier. Cette caractéristique influence la taille
de la valeur sauvegardée et le temps mis pour le faire. D'autre part,
en présence de valeurs physiquement modifiables, cela peut changer le
comportement de cette valeur après une récupération selon que le
partage a été conservé ou non.
- structures circulaires.
Dans le cas d'une valeur circulaire, la linéarisation sans partage
risque de boucler. Il sera nécessaire de conserver ce partage.
- valeurs fonctionnelles.
Les valeurs fonctionnelles, ou fermetures, se composent d'une partie
environnement et d'une partie code. La partie code correspond au point
d'entrée (adresse) du code à exécuter. Que faut-il faire alors du
code? Il est possible de stocker uniquement cette adresse, mais alors
seul le même programme trouvera un sens correct à cette adresse. Il
est aussi possible de sauver la suite d'instructions machine de cette
fonction, mais il sera nécessaire d'avoir un mécanisme de chargement
dynamique de code.
- garantie du type à la relecture.
C'est la principale difficulté de ce mécanisme. Le typage statique
garantit que les valeurs typées n'engendreront pas d'erreur de type à
l'exécution. Mais cela n'est vrai que pour les valeurs appartenant au
programme en cours d'exécution. Quel type peut-on donner à une valeur
extérieure au programme, qui n'a donc pas été vue par le vérificateur
de types? Pour seulement vérifier que la valeur relue possède le type
monomorphe engendré par le compilateur, il faudrait qu'il y ait une
transmission de ce type au moment de la sauvegarde, puis vérification
au chargement. Avec de surcroît un mécanisme de gestion de
versions des types, pour rester sûr, même en cas de redéclaration
d'un type dans le programme.
Module Marshal
Le mécanisme de linéarisation du module Marshal permet, au
choix, de retenir ou non le partage des valeurs traitées. Il permet
également de traiter les fermetures, mais, dans ce cas, seul le
pointeur de code est conservé.
Ce module comporte principalement les fonctions de linéarisation vers
un canal ou une chaîne et les fonctions de récupération à partir
d'un canal ou d'une chaîne. Les fonctions de linéarisation sont
paramétrables. Le type suivant déclare les deux options possibles :
type external_flag =
No_sharing
| Closures;;
Le constructeur constant No_sharing indique de ne pas
conserver le partage d'une valeur, par défaut il l'est. Le
constructeur Closures permet de traiter les fermetures en
conservant son pointeur de code. Son absence déclenchera une exception
si l'on essaie de conserver une valeur fonctionnelle.
Warning
Le constructeur Closures est inopérant en mode interactif. Il
ne peut être utilisé qu'en mode ligne de commande.
Les fonctions d'écriture et de lecture de ce module sont regroupées à
la figure 8.4.
to_channel |
: |
out_channel -> 'a -> extern_flag list -> unit |
to_string |
: |
'a -> extern_flag list -> string |
to_buffer |
: |
string -> int -> int -> 'a -> extern_flag list -> unit |
from_channel |
: |
in_channel -> 'a |
from_string |
: |
string -> int -> 'a |
Figure 8.4 : Fonctions du module Marshal
La fonction to_channel prend un canal de sortie, une valeur
et une liste d'options et écrit la valeur passée sur le canal. La
fonction to_string produit une chaîne correspondant à la
valeur linéarisée, alors que to_buffer effectue
le même travail en modifiant une partie d'une chaîne passée en
argument. La fonction from_channel lit sur un canal une
valeur linéarisée et la retourne. La variante from_string
prend en entrée une chaîne et la position du premier caractère à
lire dans la chaîne. Plusieurs valeurs linéarisées peuvent être
stockées dans le même fichier ou dans la même chaîne. Dans le
premier cas, elles pourront être lues séquentiellement. Dans le
deuxième cas, il faudra préciser le bon décalage par rapport au début
de la chaîne pour décoder la valeur désirée.
# let
s
=
Marshal.to_string
[
1
;2
;3
;4
]
[]
in
String.sub
s
0
1
0
;;
- : string = "\132\149\166\190\000\000\000\t\000\000"
Warning
L'utilisation de ce module fait perdre la sûreté du typage statique
(voir infra, page ??).
La relecture d'un objet persistant crée une valeur de type
indéterminé :
# let
x
=
Marshal.from_string
(Marshal.to_string
[
1
;
2
;
3
;
4
]
[])
0
;;
val x : '_a = <poly>
Cette indétermination est marquée, en Objective CAML, par la variable
de type faible '_a. Il est recommandé de spécifier le
type attendu :
# let
l
=
let
s
=
(Marshal.to_string
[
1
;
2
;
3
;
4
]
[])
in
(Marshal.from_string
s
0
:
int
list)
;;
val l : int list = [1; 2; 3; 4]
Nous revenons page ?? sur ce point.
Remarque
La fonction output_value de la bibliothèque préchargée
correspond à l'appel de to_channel avec une liste d'options
vide. La fonction input_value du module Pervasives
appelle directement la fonction from_channel. Celles-ci ont
été conservées pour la compatibilité des anciens programmes.
Exemple : sauvegarde d'écrans
On cherche à sauvegarder le bitmap, représenté comme matrice de
couleurs, de toute la fenêtre graphique. La fonction
save_screen récupère le bitmap, le convertit en tableau de
couleurs et le sauve dans un fichier dont le nom a été passé en
paramètre.
# let
save_screen
name
=
let
i
=
Graphics.get_image
0
0
(Graphics.size_x
())
(Graphics.size_y
())
in
let
j
=
Graphics.dump_image
i
in
let
oc
=
open_out
name
in
output_value
oc
j;
close_out
oc;;
val save_screen : string -> unit = <fun>
La fonction load_screen effectue l'opération inverse.
Elle ouvre le fichier dont le nom est passé en paramètre, récupère la valeur stockée à l'intérieur, convertit cette matrice couleurs en bitmap puis affiche celui-ci.
# let
load_screen
name
=
let
ic
=
open_in
name
in
let
dessin
=
((input_value
ic)
:
Graphics.color
array
array)
in
close_in
ic;
Graphics.close_graph();
Graphics.open_graph
(":0 "
^
(string_of_int(Array.length
dessin.
(0
)))
^
"x"
^
(string_of_int(Array.length
dessin)));
let
dessin2
=
Graphics.make_image
dessin
in
Graphics.draw_image
dessin2
0
0
;
dessin2
;;
val load_screen : string -> Graphics.image = <fun>
Warning
Les valeurs dont le type est abstrait ne peuvent pas être persistantes.
C'est pour cela que l'exemple précédent n'utilise pas le type
Graphics.image qui est abstrait, mais bien le type
color array array qui lui est concret. L'abstraction de types
est présentée au chapitre 14.
Partage
La perte du partage d'une donnée peut lui faire perdre complètement
son intérêt. Reprenons l'exemple du générateur de symboles de la page
??. Pour une raison quelconque, on désire sauver
les valeurs fonctionnelles new_s et reset_s, pour,
par la suite, repartir de la valeur de leur compteur commun. On écrit
alors le programme suivant :
# let
reset_s,
new_s
=
let
c
=
ref
0
in
(
function
()
->
c
:=
0
)
,
(
function
s
->
c:=!
c+
1
;
s^
(string_of_int
!
c)
)
;;
# let
save
=
Marshal.to_string
(new_s,
reset_s)
[
Marshal.
Closures;Marshal.
No_sharing]
;;
# let
(new_s1,
reset_s1)
=
(Marshal.from_string
save
0
:
((string
->
string
)
*
(unit
->
unit)))
;;
# (* 1 *)
Printf.printf
"new_s : \%s\n"
(new_s
"X"
);
Printf.printf
"new_s : \%s\n"
(new_s
"X"
);
(* 2 *)
Printf.printf
"new_s1 : \%s\n"
(new_s1
"X"
);
(* 3 *)
reset_s1();
Printf.printf
"new_s1 (après reset_s1) : \%s\n"
(new_s1
"X"
)
;;
Characters 148-154:
Unbound value new_s1
Les deux premiers affichages en (* 1 *)
sont cohérents par
rapport à la définition. L'affichage obtenu en (* 2 *)
après
relecture des fermetures semble également correct (après X2
vient X3
). Mais, en fait, le partage du compteur c
entre les fonctions relues new_s1 et reset_s1 est
perdu comme l'atteste l'affichage de X4
bien que l'on ait
entre temps remis le compteur à zéro. Chaque fermeture a
une copie du compteur et l'appel à reset_s1 ne remet pas
à zéro le compteur de new_s1. Il n'aurait donc pas fallu
utiliser l'option No_sharing lors de la linéarisation.
Il est en règle générale nécessaire de conserver le partage. Néanmoins
dans certains cas où la vitesse d'exécution est importante, l'absence
de partage effectue un parcours plus rapide à la
sauvegarde. L'exemple suivant montre une fonction qui effectue une
copie d'une matrice. Dans ce cas là aussi il peut être préférable de
casser le partage :
# let
copie_mat_f
(m
:
float
array
array)
=
let
s
=
Marshal.to_string
m
[
Marshal.
No_sharing]
in
(Marshal.from_string
s
0
:
float
array
array);;
val copie_mat_f : float array array -> float array array = <fun>
On peut aussi l'utiliser pour la création d'une matrice sans partage :
# let
create_mat_f
n
m
v
=
let
m
=
Array.create
n
(Array.create
m
v)
in
copie_mat_f
m;;
val create_mat_f : int -> int -> float -> float array array = <fun>
# let
a
=
create_mat_f
3
4
3
.
1
4
;;
val a : float array array =
[|[|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 3.14; 3.14|];
[|3.14; 3.14; 3.14; 3.14|]|]
# a.
(1
).
(2
)
<-
6
.
2
8
;;
- : unit = ()
# a;;
- : float array array =
[|[|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 6.28; 3.14|];
[|3.14; 3.14; 3.14; 3.14|]|]
Ce qui est un comportement plus habituel que celui de
Array.create et ressemble à celui de
Array.create_matrix.
Taille des valeurs
Il peut être utile de connaître la taille d'un persistant. Si le
partage est conservé, cette taille reflète assez bien l'occupation
mémoire d'une valeur. Bien que le codage optimise parfois la taille
des valeurs immédiates2, l'information de taille de leur codage respectif permet de
comparer différentes implantations d'une structure de
données. D'autre part pour les programmes qui ne s'arrêtent jamais,
logiciels embarqués ou même serveurs réseau, l'étude de la taille des
données peut indiquer des fuites de mémoire. Le module
Marshal a deux fonctions de calcul de la taille et une constante
qui sont décrits à la figure 8.5.
header_size |
: |
int |
data_size |
: |
string -> int -> int |
total_size |
: |
string -> int -> int |
Figure 8.5 : Fonctions de taille de Marshal
La taille totale d'un persistant est égale à la taille de ses données
additionnée à la taille de l'en-tête.
Nous donnons ci-dessous un petit exemple d'utilisation du codage MD5
pour comparer deux représentations des arbres binaires :
# let
taille
x
=
Marshal.data_size
(Marshal.to_string
x
[])
0
;;
val taille : 'a -> int = <fun>
# type
'a
bintree1
=
Empty1
|
Node1
of
'a
*
'a
bintree1
*
'a
bintree1
;;
type 'a bintree1 = | Empty1 | Node1 of 'a * 'a bintree1 * 'a bintree1
# let
t1
=
Node1(2
,
Node1(1
,
Node1(0
,
Empty1,
Empty1),
Empty1),
Node1(3
,
Empty1,
Empty1))
;;
val t1 : int bintree1 =
Node1
(2, Node1 (1, Node1 (0, Empty1, Empty1), Empty1),
Node1 (3, Empty1, Empty1))
# type
'a
bintree2
=
Empty2
|
Leaf2
of
'a
|
Node2
of
'a
*
'a
bintree2
*
'a
bintree2
;;
type 'a bintree2 =
| Empty2
| Leaf2 of 'a
| Node2 of 'a * 'a bintree2 * 'a bintree2
# let
t2
=
Node2(2
,
Node2(1
,
Leaf2
0
,
Empty2),
Leaf2
3
)
;;
val t2 : int bintree2 = Node2 (2, Node2 (1, Leaf2 0, Empty2), Leaf2 3)
# let
s1,
s2
=
taille
t1,
taille
t2
;;
val s1 : int = 13
val s2 : int = 9
Les valeurs données par la fonction taille reflètent bien
l'intuition que l'on peut avoir de la taille de t1 et
t2.
Problème de typage
Le vrai problème avec les persistants est qu'il est possible de casser
le système de types d'Objective CAML. Les fonctions de création retournent
bien un type monomorphe (unit ou string). En
revanche, les fonctions de récupération retournent un type polymorphe
'a. Du point de vue des types, on pourra faire n'importe quel
usage d'une valeur persistante. Voici le pire d'entre eux (voir
chapitre 2, page ??) : créer
une fonction copie_magique de type 'a -> 'b.
# let
copie_magique
a
=
let
s
=
Marshal.to_string
a
[
Marshal.
Closures]
in
Marshal.from_string
s
0
;;
val copie_magique : 'a -> 'b = <fun>
L'utilisation d'une telle fonction provoque un arrêt brutal de
l'exécution.
# (copie_magique 3 : float) +. 3.1;;
Segmentation fault
En mode interactif (sous Linux), on sort même de la boucle
d'interaction avec un signal d'erreur système correspondant à une
violation de mémoire.
Interface avec le système
La bibliothèque standard comporte six modules d'interface avec le
système :
-
module Sys : pour la communication entre le système
d'exploitation et le programme ;
- module Arg : pour analyser les paramètres passés au
programme sur la ligne de commande ;
- module Filename : pour la navigation dans les
répertoires (indépendamment du système) ;
- module Printexc : pour l'interception et l'affichage
des exceptions ;
- module Gc : pour le contrôle du mécanisme de
récupération automatique de mémoire qui est décrit au chapitre
9 ;
- module Callback : pour l'appel de fonctions Objective CAML à
partir de C qui est décrit au chapitre 12.
Les quatre premiers modules sont décrits ci-dessous.
Module Sys
Ce module apporte de bien utiles fonctions de communication avec
le système d'exploitation, ainsi que le traitement des signaux reçus
par un programme. Les valeurs de la figure 8.6 donnent
des informations sur le système.
OS_type |
: |
string |
|
|
type du système |
interactive |
: |
bool ref |
|
|
vrai si exécution au toplevel |
word_size |
: |
string |
|
|
taille d'un mot (32 ou 64 bits) |
max_string_length |
: |
int |
|
|
taille maximale d'une chaîne |
max_array_length |
: |
int |
|
|
taille maximale d'un vecteur |
time |
: |
unit -> float |
|
|
donne le temps en secondes depuis le début du programme |
Figure 8.6 : Informations sur le système
La communication entre le programme et le système passe par la ligne
de commande, la valeur d'une variable de l'environnement
d'exécution et la possibilité de lancer un autre programme. Ces
fonctions sont décrites à la figure 8.7.
argv |
: |
string array |
|
|
contient le vecteur de paramètres |
getenv |
: |
string -> string |
|
|
demande de la valeur d'une variable |
command |
: |
string -> int |
|
|
exécution de la commande passée en argument |
Figure 8.7 : Communication avec le système
Les fonctions de la figure 8.8 permettent
une navigation dans la hiérarchie de fichiers.
file_exists |
: |
string -> bool |
|
|
retourne true si le fichier existe |
remove |
: |
string -> unit |
|
|
destruction d'un fichier |
rename |
: |
string -> string -> unit |
|
|
renommage d'un fichier |
chdir |
: |
string -> unit |
|
|
change de catalogue courant |
getcwd |
: |
unit -> string |
|
|
retourne le nom du catalogue courant |
Figure 8.8 : Manipulation de fichiers
Enfin la gestion des signaux sera décrite au chapitre sur la
programmation système (18).
Voici un petit programme qui reprend l'exemple sur la sauvegarde d'une
fenêtre graphique sous forme d'un tableau de couleurs. La fonction
main vérifie qu'elle n'est pas lancée de la boucle d'interaction, lit les
noms des fichiers à visualiser sur la ligne de
commande, teste s'ils existent et les visualise (avec la
fonction load_screen). On intercale une attente clavier dans la
fenêtre graphique entre deux visualisations.
# let
main
()
=
if
not
(!
Sys.interactive)
then
for
i
=
0
to
Array.length(Sys.argv)
-
1
do
let
nom
=
Sys.argv.
(i)
in
if
Sys.file_exists
nom
then
begin
ignore(load_screen
nom);
ignore(Graphics.read_key)
end
done;;
val main : unit -> unit = <fun>
Module Arg
Le module Arg fixe une petite syntaxe pour les arguments de
la ligne de commande. Il fournit une fonction permettant l'analyse
syntaxique ainsi que la définition d'une action associée aux
éléments analysés.
Les divers éléments de la ligne de commande sont séparés par
un ou plusieurs espaces. Ils constituent les valeurs stockées dans
le tableau Sys.argv. Dans la syntaxe fixée par
Arg, certains éléments distingués
commencent par le caractère moins (-). On les appelle des
mots clés de la ligne de commande. On peut associer aux mots clés une
action spécifique qui peut prendre en argument une valeur de type
string, int ou float. La valeur de ces
arguments est initialisée avec la valeur trouvée sur la ligne de
commande juste après le mot clé. Il y a dans ce cas appel à une
fonction de conversion des chaînes de caractères vers le type
attendu. Les autres éléments de la ligne de commande sont dits
arguments anonymes. On leur associe une action commune qui prend
leur valeur en argument. Une option non définie provoque
l'affichage d'une petite documentation en ligne de la commande dont le
contenu est défini par l'utilisateur.
Les actions associées aux mots clés sont encapsulées dans le
type :
type
spec
=
|
Unit
of
(unit
->
unit)
(* Call the function with unit argument*)
|
Set
of
bool
ref
(* Set the reference to true*)
|
Clear
of
bool
ref
(* Set the reference to false*)
|
String
of
(string
->
unit)
(* Call the function with a string
argument *)
|
Int
of
(int
->
unit)
(* Call the function with an int
argument *)
|
Float
of
(float
->
unit)
(* Call the function with a float
argument *)
|
Rest
of
(string
->
unit)
(* Stop interpreting keywords and call the
function with each remaining argument*)
La fonction d'analyse de la ligne de commande est :
# Arg.parse
;;
- : (string * Arg.spec * string) list -> (string -> unit) -> string -> unit =
<fun>
Son premier argument est une liste de triplets de la forme
(key, spec, doc) tels que :
-
key est une chaîne de caractères correspondant à
un mot clé. Elle commence donc par le caractère réservé '_'.
- spec est une valeur de type spec spécifiant
l'action associée à key.
- doc est une chaîne de caractères décrivant
l'option key. Elle est affichée sur une erreur de syntaxe.
Le deuxième argument est la fonction de traitement des arguments
anonymes de la ligne de commande.
Le dernier argument est une chaîne de caractères affichée en
tête de la documentation en ligne.
Le module Arg comprend également :
-
Bad : exception prenant en argument une chaîne de
caractères. Elle peut être utilisée par les fonctions de
traitement.
- usage : de type (string * Arg.spec * string)
list -> string -> unit, cette fonction affiche la documentation
en ligne. On lui fournira de préférence d les mêmes arguments que
ceux de parse.
- current : de type int ref qui contient une
référence sur la valeur courante de l'indice dans le tableau
Sys.argv. On peut donc modifier cette valeur si besoin est.
À titre d'exemple, nous donnons la fonction read_args
permettant d'initialiser la configuration du démineur vu au chapitre
6, page ??. Les options possibles
seront -col, -lig et -min. Elles seront
suivies d'un entier précisant, respectivement : le nombre de
colonnes, le nombre de lignes et le nombre de mines désirées. Ces
valeurs ne devront pas
être inférieures aux valeurs respectives par défaut 10,
10 et 15.
Les fonctions de traitement sont :
# let
set_nbcols
cf
n
=
cf
:=
{!
cf
with
nbcols
=
n}
;;
# let
set_nbrows
cf
n
=
cf
:=
{!
cf
with
nbrows
=
n}
;;
# let
set_nbmines
cf
n
=
cf
:=
{!
cf
with
nbmines
=
n}
;;
Elles sont toutes trois de type config ref -> int -> unit.
La fonction d'analyse de la ligne de commande peut s'écrire :
# let
read_args()
=
let
cf
=
ref
default_config
in
let
speclist
=
[
("-col"
,
Arg.
Int
(set_nbcols
cf),
"nombre de colonnes (>=10)"
);
("-lig"
,
Arg.
Int
(set_nbrows
cf),
"nombre de lignes (>=10)"
);
("-min"
,
Arg.
Int
(set_nbmines
cf),
"nombre de mines (>=15)"
)]
in
let
usage_msg
=
"usage : demin [-col n] [-lig n] [-min n]"
in
Arg.parse
speclist
(fun
s
->
())
usage_msg;
!
cf
;;
val read_args : unit -> config = <fun>
Elle calcule une configuration qui sera passée en argument à la
fonction d'ouverture de la fenêtre du jeu open_wcf lors du
lancement du jeu. Chaque option est, comme son nom l'indique,
optionnelle. Si elle ne figure pas sur la ligne de commande, le
paramètre correspondant garde la valeur par défaut. L'ordre des
options n'a pas d'importance.
Module Filename
Le module Filename donne des opérations sur les noms de
fichiers indépendantes des systèmes d'exploitation. En effet les
conventions de nommage des fichiers et des catalogues diffèrent
fortement entre Windows, Unix et MacOS.
Module Printexc
Ce module très court (trois fonctions décrites à la figure 8.9)
apporte un récupérateur général d'exceptions. Cela est
particulièrement pratique pour les programmes exécutés en mode
commande3 pour être sûr de ne pas laisser s'échapper une exception
qui arrêterait le programme.
catch |
: |
('a -> 'b) -> 'a -> 'b |
|
|
récupérateur général |
print |
: |
('a -> 'b) -> 'a -> 'b |
|
|
affiche et redéclenche l'exception |
to_string |
: |
exn -> string |
|
|
convertit une exception en chaîne |
Figure 8.9 : Récupération d'exceptions
La fonction catch applique son premier argument à son
second. Cela lancera la fonction principale du programme. Si une
exception arrive au niveau de catch, c'est à dire n'est pas
récupérée à l'intérieur du programme, alors catch affichera
son nom et sortira du programme. La fonction print a le même
comportement que catch mais redéclenchera l'exception après
l'affichage. Enfin la fonction to_string convertit une
exception en chaîne de caractères. Elle est utilisée par les deux
fonctions précédentes. Si on reprend la fonction main de
visualisation de bitmaps, on écrira alors une fonction
d'encapsulation
go de la manière suivante :
# let
go
()
=
Printexc.catch
main
();;
val go : unit -> unit = <fun>
Cela permet de terminer normalement le programme en affichant la
valeur de l'exception non capturée.