Précédent Index Suivant

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 :

À 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 :

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 :

Structures de données linéaires

Les modules sur les structures de données linéaires sont : 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 : Signalons un dernier module manipulant des structures linéaires :
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 : De même certaines conventions sur les fonctions de parcours et de traitement se retrouvent dans un certain nombre de modules : Pour les structures dont les éléments sont indicés. on a :

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.
length : 'a t -> int

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 : 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)" 34 78 ;;
(x=34, y=78)- : unit = ()
# Printf.printf "nom = %s, age = %d" "mapom" 18 ;;
nom = mapom, age = 18- : unit = ()
# let s = Printf.sprintf "%10.5f\n%10.5f\n" (-.12.24) (2.30000008) ;;
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; -.44.22; 35.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 45 ;;
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.14 ;;
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 100 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.

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 10;;
- : 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.14;;
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.28;;
- : 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 : 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 : 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 : À 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.


Précédent Index Suivant