VII. Programmation impérative▲
Jusqu'ici nous avons étudié un sous-ensemble de Caml que l'on peut qualifier de purement fonctionnel . La seule opération mise en jeu, dans un programme purement fonctionnel, est l'application d'une fonction. Il n'existe pas d'opérations permettant de modifier explicitement l'état de la mémoire. Malgré son élégance, la programmation purement fonctionnelle atteint ses limites quand il s'agit :
- de communiquer avec le monde extérieur. Par exemple, lire des données, imprimer des résultats ;
- de modifier explicitement le contenu de certaines cases mémoires, ce qui peut être nécessaire pour optimiser des programmes ou bien lorsque les changements d'état sont inhérents à l'application traitée ;
- de spécifier l'ordre dans lequel doivent être évaluées certaines expressions.
On appelle effet de bord « une modification d'une case de la mémoire ou bien une interaction avec le monde extérieur (impression ou lecture) » 1Définition extraite du livre « Le langage Caml » de Pierre Weis et Xavier Leroy. . Un programme qui opère par une suite d'effets de bord est appelé programme impératif . Caml est un langage qui combine harmonieusement programmation fonctionnelle et programmation impérative.
C'est le sous-ensemble impératif de Caml qui fait l'objet de ce chapitre. Nous étudierons successivement les entrées-sorties, le séquencement des expressions, les valeurs modifiables et les boucles.
VII-A. Entrées-sorties▲
La gestion des entrées-sorties en Caml est classique. Pour lire ou écrire sur un fichier on utilise des canaux d'entrée ou de sortie qui sont des valeurs Caml de type in_channel ou out_channel.
Un canal d ' entrée est créé par la fonction prédéfinie open_in appliquée au nom du fichier à ouvrir. Il est fermé en lui appliquant la fonction prédéfinie close_in. La lecture sur un canal d'entrée est réalisée en lui appliquant les fonctions prédéfinies input_char qui lit le prochain caractère et input_line qui lit la prochaine ligne ; l'exception End_of_file est déclenchée lorsque la fin du fichier est atteinte.
Un canal de sortie est créé par la fonction prédéfinie open_out appliquée au nom du fichier à ouvrir. Il est fermé en lui appliquant la fonction prédéfinie close_out. L'écriture sur un canal de sortie est réalisée en lui appliquant les fonctions prédéfinies output_char qui écrit un caractère et output_string qui écrit une chaîne de caractères. Par exemple, le programme suivant écrit dans un fichier puis relit ce qu'il a écrit :
#let sortie = open_out "mon_fichier";;
sortie : out_channel = <abstract>
#output_string sortie "un\n";;
- : unit = ()
#output_string sortie "deux\n";;
- : unit = ()
#output_string sortie "12";;
- : unit = ()
#close_out sortie;;
- : unit = ()
#let entree = open_in "mon_fichier";;
entree : in_channel = <abstract>
#input_line entree;;
- : string = "un"
#input_line entree;;
- : string = "deux"
#input_char entree;;
- : char = `1`
#input_char entree;;
- : char = `2`
#input_char entree;;
Uncaught exception: End_of_file
#close_in entree;;
- : unit = ()
On notera que la valeur d'une ouverture est <abstract> et que le type d'arrivée de la fonction input_line est string.
Classiquement, la lecture de caractères saisis au clavier est un cas particulier de lecture de fichier. Elle se fait sur le canal prédéfini std_in (ou stdin). Il en est de même pour l'affichage à l'écran qui se fait sur le canal prédéfini std_out (ou std_out). Cependant par souci de simplicité Caml offre les fonctions prédéfinies suivantes :
read_line ()  input_line std_in
print_string s  output_string std_out s
print_char c  output_char std_out c
print_int i  output_string std_out (string_of_int i)
print_float f  output_string std_out (string_of_float f)
print_newline ()  output_string std_out "\n"
Voici, par exemple, deux fonctions dont nous nous servirons par la suite :
#let un_espace () = print_string " ";;
un_espace : unit -> unit = <fun>
#let a_la_ligne () = print_newline ();;
a_la_ligne : unit -> unit = <fun>
VII-B. Séquencement▲
L'opérateur ; permet d'imposer l'ordre dans lequel sont évaluées deux d'expressions. Si e 1 et e 2 sont des expressions, alors :
e1 ; e2
est une expression qui a pour effet de bord d'évaluer e 1 puis d'évaluer e 2 et qui a pour valeur la valeur de e 2 . Il est évident qu'une telle expression n'a d'intérêt que si e 1 est une expression à effet de bord.
L'opérateur ; est associatif à droite. Pour marquer clairement le début et la fin d'une séquence d'expressions, cette séquence peut être encadrée par les mots-clés begin et end.
Par exemple, le programme suivant calcule la surface d'un rectangle dont la longueur et la largeur sont demandées à l'utilisateur.
#let lon =
print_string "Donnez la longueur du rectangle en mètres ? ";
int_of_string (read_line ())
and lar =
print_string "Donnez la largeur du rectangle en mètres ? ";
int_of_string (read_line ())
in
begin
print_string "Ce rectangle a une surface de ";
print_int (lon * lar);
print_string " m2";
a_la_ligne ()
end;;
Donnez la longueur du rectangle en mètres ? 15
Donnez la largeur du rectangle en mètres ? 5
Ce rectangle a une surface de 75 m2
- : unit = ()
VII-C. Valeurs modifiables▲
VII-C-1. Références▲
Pour permettre au programmeur de modifier explicitement le contenu de la mémoire, Caml manipule des pointeurs comme en Pascal ou en C. En Caml un pointeur vers un objet de type t est appelé référence et est une instance du type t ref. Une référence est construite en appliquant le constructeur ref à l'objet à référencer. Dans le programme suivant, horloge référence un triplet constitué de l'heure, des minutes et des secondes :
#let horloge = ref (23, 21, 59);;
horloge : (int * int * int) ref = ref (23, 21, 59)
L'opérateur d'affectation := permet de remplacer l'objet référencé et l'opérateur ! permet d'y accéder :
#prefix :=;;
- : 'a ref -> 'a -> unit = <fun>
#prefix !;;
- : 'a ref -> 'a = <fun>
On remarque que l'opérateur := a pour type d'arrivée unit puisqu'il se réduit à un effet de bord : la modification de la mémoire. Le programme suivant fait avancer l'horloge d'une seconde :
#let avancer_horloge () =
horloge := match !horloge with
| (h, 59, 59) -> (h + 1, 0, 0)
| (h, m, 59) -> (h, m + 1, 0)
| (h, m, s) -> (h, m, s + 1);;
avancer_horloge : unit -> unit = <fun>
#avancer_horloge ();;
- : unit = ()
#horloge;;
- : (int * int * int) ref = ref (23, 22, 0)
Comme les autres valeurs Caml, les références peuvent être filtrées. Si F est un filtre de type t alors ref F est un filtre de type t ref. Par exemple :
#match horloge with ref (h, _, _) -> h;;
- : int = 23
VII-C-2. Tableaux▲
Un tableau (ou vecteur ) est une suite modifiable, de longueur fixe et non nulle, de valeurs du même type que nous appellerons éléments du tableau. Un tableau de valeurs de type t est une instance du type t vect. Il est noté en séparant chacun de ses éléments par un ; et en encadrant cette suite par les signes [| et |]. Par exemple le tableau [|15.0; 5.5; 10.0|] est de type float vect.
Un tableau peut être vu comme une suite de n cases numérotées de 0 à n ƒ{ 1 dont chacune contient une valeur. Le contenu des cases est initialisé à la construction du tableau et peut être modifié par la suite. Un tableau peut être construit de deux façons différentes :
-
soit en donnant la liste de ses éléments. Par exemple :
Sélectionnez
#[|1 + 9; 2 + 8; 3 + 7; 4 + 6; 5 + 5|];; - : int vect = [|10; 10; 10; 10; 10|]
- soit à l'aide du constructeur make_vect en donnant le nombre d'éléments et une valeur initiale qui sera affectée à chacun de ces éléments. Par exemple :
#make_vect 4 "";;
- : string vect = [|""; ""; ""; ""|]
La fonction prédéfinie vect_length calcule la longueur du tableau auquel elle s'applique. Par exemple :
#let saisons = make_vect 4 "";;
saisons : string vect = [|""; ""; ""; ""|]
#vect_length saisons;;
- : int = 4
L'opérateur .() permet d'extraire la valeur contenue dans une case d'un tableau et l'opérateur <- d'affecter une valeur à une case d'un tableau. L'expression t .( i ) a pour valeur la valeur contenue dans la ie case du tableau t et l'expression t .( i ) <- v a pour effet de bord d'affecter la valeur v à la ie case du tableau t . Par exemple :
#begin
saisons.(0) <- "printemps";
saisons.(1) <- "été";
saisons.(2) <- "automne";
saisons.(3) <- "hiver";
end;;
- : unit = ()
#saisons.(3);;
- : string = "hiver"
Nous verrons au §VI.D comment se servir des boucles pour parcourir les éléments d'un tableau.
VII-C-3. Enregistrements à champs modifiables▲
Un enregistrement peut avoir des champs modifiables, c'est-à-dire des champs dont on peut changer la valeur. Il suffit que dans la définition du type de cet enregistrement ces champs soient précédés du mot clé mutable. Par exemple :
#type personne = {Nom: string; Prénom: string; mutable Age: int};;
Type personne defined.
Le champ Age d'un enregistrement de type personne est modifiable.
Pour remplacer la valeur d'un champ modifiable c d'un enregistrement e on utilise la construction e . c <- v où v est la nouvelle valeur du champ c . Par exemple le programme suivant augmente l'âge d'une personne :
#let dupont = {Nom = "Dupont"; Prénom = "Jean"; Age = 42};;
dupont : personne = {Nom="Dupont"; Prénom="Jean"; Age=42}
#dupont.Age <- dupont.Age + 1;;
- : unit = ()
#dupont;;
- : personne = {Nom="Dupont"; Prénom="Jean"; Age=43}
VII-D. Boucles▲
Caml offre deux sortes de boucles pour répéter l'évaluation d'expressions à effet de bord : la boucle while et la boucle for.
Si e 1 est une expression booléenne et e 2 une expression quelconque, l'expression :
while e1 do e2 done
a pour effet de bord de répéter l'évaluation de l'expression e 2 tant que l'expression e 1 est vraie. L'expression e 2 est évaluée au début de chaque itération. Par exemple, le programme suivant affiche la suite des chiffres de 0 à 9 :
#let i = ref(0) in
while !i < 10 do
print_int !i;
un_espace ()
done;
a_la_ligne ();;
0 1 2 3 4 5 6 7 8 9
- : unit = ()
Si i , d et f sont des entiers et e est une expression quelconque, l'expression :
for i = d to (resp. downto) f do e done
a pour effet de répéter l'évaluation de l'expression e avec i = d , i = d + 1, …, i = f (resp. i = d , i = d ƒ{ 1, …, i = f ). Si f < d (resp. f > d ) l'expression e n'est jamais évaluée. Par exemple, le programme suivant calcule la somme des éléments d'un tableau d'entiers t :
#let t = [|2; 4; 6; 8; 10; 12|] and s = ref 0 in
for i = 0 to (vect_length t) - 1 do
s := !s + t.(i)
done;
!s;;
- : int = 42