Académique Documents
Professionnel Documents
Culture Documents
Initiation à la
programmation fon
tionnelle
Jean-Christophe Filliâtre
Table des matières
1 Fondamentaux 5
1.1 Premiers pas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.1 Le premier programme . . . . . . . . . . . . . . . . . . . . . . . . 5
1.1.2 Dé
larations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.1.3 Référen
es . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.1.4 Expressions et instru
tions . . . . . . . . . . . . . . . . . . . . . . 8
1.1.5 Variables lo
ales . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2 La bou
le d'intera
tion o
aml . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3 Fon
tions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.1 Syntaxe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.2 Fon
tions
omme valeurs de première
lasse . . . . . . . . . . . . 13
1.3.3 Fon
tions ré
ursives . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.3.4 Polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4 Allo
ation mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.1 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.2 Enregistrements . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.4.3 N-uplets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.4.4 Listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.4.5 Types
onstruits . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.5 Ex
eptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2 Modules 28
2.1 Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.1.1 Fi
hiers et modules . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.1.2 En
apsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.1.3 Langage de modules . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.2 Fon
teurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3 Persistan
e 37
3.1 Stru
tures de données immuables . . . . . . . . . . . . . . . . . . . . . . 37
3.2 Intérêts pratiques de la persistan
e . . . . . . . . . . . . . . . . . . . . . 39
3.3 Interfa
e et persistan
e . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2
Introdu
tion
Ce
ours est une introdu
tion au langage de programmation O
aml et plus générale-
ment à la programmation fon
tionnelle. Cette introdu
tion est né
essairement su
in
te
il s'agit d'un
ours de quatre heures seulement et ne peut don
ni entrer dans les
détails ni même être exhaustive. Pour une introdu
tion détaillée au langage O
aml on
se référera au poly
opié de C. Mar
hé et R. Treinen Formation au langage Caml dont
une
opie a
ompagne le présent do
ument.
Le but de
ette initiation est plutt d'introduire un
ertain nombre d'idées
lés
on
er-
nant la programmation fon
tionnelle, ainsi que de démonter d'autres idées, fausses, sur
elle-
i. Il ne s'agit nullement d'armer qu'O
aml est le meilleur des langages de pro-
grammation ; une telle armation n'aurait d'ailleurs au
un sens, les langages de pro-
grammation ne pouvant être
omparés de manière absolue. Comme on a
outume de le
dire,
il n'y a pas de bon langage de programmation,
il n'y a que de bons programmeurs
Un
ertain nombre de
on
epts exposés dans
e
ours se retrouvent d'ailleurs dans d'autres
langages de programmation (ex
eptions, GC) et d'autres en
ore sont valables quel que
soit le langage utilisé (notion de persistan
e).
Pour de plus amples informations sur le langage O
aml (
omment le ré
upérer, quels
sont les livres ou les bibliothèques de programmes existants, les listes de dis
ussion, et
.)
on
onsultera le site web o
iel :
aml.inria.fr
Ce
ours a été é
rit en utilisant la version 3.08 d'O
aml mais la majorité sinon la
totalité des exemples fon
tionnent ave
des versions antérieures et en toute probabi-
lité ave
les versions futures du langage. Nous supposerons également un environnement
Unix, sans que
ela ne dépasse l'aspe
t ane
dotique.
3
Coordonnées de l'auteur :
Jean-Christophe Filliâtre
Tél : 01-69-15-65-76
Bureau 144
Email : filliatrlri.fr
Web : http://www.lri.fr/~filliatr/
4
Chapitre 1
Fondamentaux
Le programme le plus
élèbre, par
e que le premier , est
elui qui se
ontente
d'a
her hello world ! sur la sortie standard. En O
aml il s'é
rit ainsi
print_string "hello world!\n"
Si le texte
i-dessus est
ontenu dans un
hier hello.ml, il est
ompilé en un exé
utable
hello ave
la
ommande
5
% o
aml
-o hello hello.ml
exa
tement
omme ave
un
ompilateur C, puis
et exé
utable peut être lan
é, donnant
le résultat es
ompté :
% ./hello
hello world!
%
On
onstate tout de suite deux diéren
es importantes ave
le même programme é
rit
en C ou en Java.
D'une part, l'appli
ation d'une fon
tion s'é
rit en O
aml par simple juxtaposition de
la fon
tion et de son argument. À la diéren
e de la plupart des langages où l'appli
ation
de f à x doit s'é
rire f(x), on se
ontente i
i d'é
rire f x. Comme dans les autres langages,
les parenthèses peuvent et doivent être utilisées lorsque les priorités des opérateurs
l'exigent,
omme dans l'expression 2*(1+3). Rien n'interdit don
en parti
ulier d'é
rire
print_string("hello world!\n")
mais les parenthèses autour de la
haîne de
ara
tères sont tout simplement inutiles.
On
onstate d'autre part qu'il n'est nul besoin de dénir une fon
tion prin
ipale
main
ontenant le
ode à exé
uter. Le programme O
aml est
onstitué i
i d'une simple
expression à évaluer. Cette expression est l'appli
ation d'une fon
tion, print_string, à
un argument, la
haîne de
ara
tères "hello world!\n". On aurait très bien pu é
rire
un programme se réduisant à
1+2
qui aurait eu pour eet de
al
uler le résultat de 1+2. Mais rien n'aurait été a
hé. Pour
ela, il faudrait é
rire par exemple le programme print_int (1+2).
1.1.2 Dé larations
Plus généralement, un programme O
aml est
onstitué d'une suite quel
onque d'ex-
pressions à évaluer et de dé
larations, séparées par un double point-virgule ;;. Une dé
la-
ration ae
te le résultat de l'évaluation d'une expression à une variable, et est introduite
par le mot
lé let. Ainsi le programme suivant
let x = 1 + 2;;
print_int x;;
let y = x * x;;
print_int y;;
al
ule le résultat de 1+2, l'ae
te à la variable x, a
he la valeur de x, puis
al
ule le
arré de x, l'ae
te à la variable y et enn a
he la valeur de y.
Une dé
laration telle que let x = 1+2 peut être vue
omme l'introdu
tion d'une
variable globale x. Mais il y a là beau
oup de diéren
es ave
la notion usuelle de
variable globale :
6
1. La variable est né
essairement initialisée, i
i par le résultat de 1+2 (en C une variable
non initialisée peut
ontenir n'importe quelle valeur ; en Java une variable non
initialisée se voit donner une valeur par défaut, qui sera toujours la même, mais
e
n'est pas la même
hose qu'exiger une initialisation de la part du programmeur).
2. Le type de la variable n'a pas besoin d'être dé
laré, il est inféré par le
ompilateur
(nous verrons
omment dans la se
tion 1.3) ; i
i le type inféré est int, le type des
entiers relatifs. Comme tout autre
ompilateur, le
ompilateur O
aml vérie que
les expressions sont bien typées (pour rejeter des expressions telles que 1+true)
mais l'utilisateur n'a pas besoin d'indiquer de types dans son programme.
3. Le
ontenu de la variable n'est pas modiable ; en d'autre termes, la variable x
ontiendra la valeur 3 jusqu'à la n du programme (nous verrons dans un instant
qu'il existe aussi des variables modiables en pla
e).
1.1.3 Référen es
Si l'on souhaite utiliser une variable modiable en pla
e, il faut l'introduire à l'aide
du mot
lé supplémentaire ref :
let x = ref 1;;
Une telle variable est appelée une référen
e. De la même manière que pour une variable
immuable, elle doit être né
essairement initialisée et son type est automatiquement inféré
par le
ompilateur. On peut alors modier le
ontenu de x ave
la même syntaxe qu'en
Pas
al :
x := 2;;
En revan
he l'a
ès à la valeur de x doit s'é
rire !x. Voi
i un exemple de programme
utilisant une référen
e :
let x = ref 1;;
print_int !x;;
x := !x + 1;;
print_int !x;;
Cette syntaxe peut paraître lourde mais elle sera justiée plus loin. Rien n'empê
he d'uti-
liser la variable x dire
tement, mais elle désigne alors la référen
e elle-même et non son
ontenu, pour un passage par référen
e par exemple. Le typage expli
ite
ette distin
-
tion, en donnant le type int ref à une référen
e
ontenant un entier. C'est exa
tement la
même situation qu'ave
les pointeurs du C, où l'on distingue le pointeur x et son
ontenu
*x, si
e n'est que l'arithmétique de pointeurs n'est pas possible et qu'il n'existe pas
d'équivalent au pointeur null.
On voit don
qu'O
aml propose deux sortes de variables : des variables modiables
en pla
e, les référen
es, semblables à
e que l'on trouve dans les langages impératifs,
mais également des variables immuables dont le
ontenu ne peut être modié. On ne
trouve pas vraiment d'équivalent dans les langages C ou Java, où seule la notion de
variable modiable en pla
e existe même si les mots
lés
onst et final y permettent
respe
tivement de dé
larer une variable
omme non modiable.
7
1.1.4 Expressions et instru
tions
Une autre spé
i
ité de la programmation fon
tionnelle, assez déroutante pour le
débutant, est l'absen
e de distin
tion entre expressions et instru
tions. Dans les lan-
gages impératifs,
e sont deux
atégories syntaxiques bien distin
tes : une
onditionnelle
if-then-else ou une bou
le for n'est pas a
eptée en position d'expression, et inverse-
ment
ertaines expressions ne sont pas autorisées en position d'instru
tion. Ainsi on ne
peut pas é
rire en Java une expression telle que
1 + (if (x == 0) f(); else g();)
ou bien une instru
tion telle que
2 * { int s = 0; for (int i = 0; i < 10; i++) s += i; return s; };
Certaines
onstru
tions seulement peuvent apparaître autant
omme expression que
omme
instru
tion, telles que l'ae
tation ou l'appel de fon
tion.
En O
aml, il n'y a pas de telle distin
tion expression/instru
tion : il n'y a que des
expressions. Ainsi on peut é
rire
1 + (if x = 0 then 2 else 3)
ar la
onstru
tion if-then-else est une expression
omme une autre. Elle s'évalue
de manière évidente : sa première opérande est évaluée et si le résultat vaut true la
deuxième opérande est évaluée et son résultat est
elui de toute l'expression if ; sinon
'est la troisième opérande qui est évaluée et donne le résultat de toute l'expression.
On retrouve toutes les
onstru
tions usuelles de la programmation impérative
omme
autant d'expressions O
aml. La séquen
e s'é
rit ave
le traditionnel point-virgule,
omme
dans
x := 1; 2 + !x
mais n'est autre qu'une expression qui évalue sa première opérande, ignore son résultat et
évalue sa se
onde opérande dont le résultat est
elui de la séquen
e en tant qu'expression.
On peut don
é
rire par exemple :
3 * (x := 1; 2 + !x)
même si
e style ne doit pas être en
ouragé
ar di
ile à lire. La notion usuelle de blo
(les a
olades en C et Java) est i
i réalisée par une simple paire de parenthèses. Pour
plus de lisibilité on peut également utiliser les mots
lés begin/end en lieu et pla
e d'une
paire de parenthèses.
De même, une bou
le for est une expression
omme une autre, ayant la syntaxe
suivante :
for i = 1 to 10 do x := !x + i done
où la variable indi
e i est immuable et de portée limitée au
orps de la bou
le. Une
telle expression doit avoir un type et une valeur,
omme toute autre expression, mais une
bou
le for n'a pas lieu de retourner une valeur parti
ulière. Pour
ela, O
aml introduit
un type prédéni appelé unit possédant une unique valeur notée (). C'est
e type qui
8
est donné à une bou
le for, ainsi qu'à une ae
tation à la diéren
e de C et Java, en
eet, O
aml ne donne pas à une ae
tation la valeur ae
tée.
En parti
ulier, la valeur () et le type unit sont automatiquement donnés à la bran
he
else d'une
onstru
tion if lorsque
elle-
i est absente. On peut ainsi é
rire
if !x > 0 then x := 0
mais en revan
he on ne pourra pas é
rire
2 + (if !x > 0 then 1)
ar une telle expression est mal typée : la bran
he then est un expression de type int alors
que la bran
he else est une expression de type unit (le message d'erreur
orrespondant
peut être parfois déroutant pour le débutant). Il est parfaitement logique qu'une telle
expression soit rejetée : sinon, quelle valeur le
ompilateur pourrait-il bien donner à
ette
expression lorsque le test !x > 0 se révèle faux ?
Comme dans tout autre langage, il existe en O
aml une notion de variable lo
ale. En
C ou Java la lo
alité d'une variable est dénie par le blo
dans lequel elle est introduite.
On é
rira ainsi
{
int x = 1;
...
}
et la portée de la variable x s'étend jusqu'à la n du blo
. En C, les variables lo
ales
ne peuvent être introduites qu'au début du blo
, alors qu'en Java elles peuvent être
introduites n'importe où dans le blo
, mais la règle de portée reste la même.
En O
aml, la notion de variable lo
ale n'est pas liée à la notion de blo
(qui i
i
n'existe pas). Elle est introduite par la
onstru
tion let in qui introduit une variable
lo
alement à une expression,
omme dans
let x = 10 in 2 * x
Comme pour la dé
laration d'une variable globale, la variable est né
essairement initiali-
sée, immuable et son type est inféré. Sa portée est exa
tement l'ensemble de l'expression
qui suit le mot
lé in. La
onstru
tion let in est une expression
omme une autre, et
on peut ainsi é
rire par exemple
let x = 1 in (let y = 2 in x + y) * (let z = 3 in x * z)
Bien entendu on peut introduire une variable modiable en pla
e ave
l'adjon
tion du
mot
lé ref
omme pour une dé
laration globale. Voi
i en parallèle un programme Java
et son équivalent O
aml :
{ int x = 1; let x = ref 1 in
x = x + 1; x := !x + 1;
int y = x * x; let y = !x * !x in
System.out.print(y); } print_int y
9
Deux remarques sont né
essaires. D'une part on
onstate que la pré
éden
e de la
onstru
-
tion let in est plus faible que
elle de la séquen
e, permettant ainsi un style très
ompa-
rable à
elui de Java (ou de C). D'autre part la variable lo
ale y n'est pas une référen
e
mais une variable immuable : n'étant pas modiée par le programme, elle n'a pas lieu
d'être une référen
e. Cela allège le programme et évite les erreurs. D'une manière géné-
rale, il est judi
ieux de préférer l'utilisation de variables immuables autant que possible,
ar
ela améliore la lisibilité du programme, sa
orre
tion et son e
a
ité. Cela ne veut
pas dire qu'il faut tomber dans l'ex
ès : les référen
es sont parfois très utiles, et il serait
regrettable de
her
her à s'en passer systématiquement (
e que font les programmeurs
Haskell).
Ré
apitulation
#
et l'on se retrouve invité (par le signe # appelé justement invite en français, prompt en
anglais) à entrer une expression ou une dé
laration O
aml à l'aide du
lavier. Le
as
é
héant, la phrase est analysée syntaxiquement, typée, évaluée et le résultat est a
hé.
# let x = 1 in x + 2;;
- : int = 3
#
I
i le système indique que l'expression est de type int et que sa valeur est 3. On retrouve
alors l'invite, et
e indéniment. C'est pourquoi on parle de bou
le d'intera
tion (toplevel
en anglais). Si l'on entre une dé
laration,
elle-
i est typée et évaluée de la même manière :
# let y = 1 + 2;;
val y : int = 3
I
i le système indique de plus qu'il s'agit d'une variable nommée y. Les dé
larations et
expressions se font suite exa
tement
omme dans le texte d'un programme. La variable
y peut don
être maintenant utilisée :
10
# y * y;;
- : int = 9
Cette bou
le d'intera
tion est très utile pour apprendre le langage ou é
rire de
ourts
mor
eaux de
ode et les tester immédiatement, ave
la possibilité d'examiner types et
valeurs de manière immédiate.
Dans la suite de
e
ours, nous utiliserons souvent le résultat d'une évaluation dans
le toplevel o
aml, de manière à visualiser immédiatement le type et/ou la valeur.
Le le
teur était sans doute impatient d'en arriver aux fon
tions, puisqu'il s'agit de
programmation fon
tionnelle. Dans un premier temps, il n'y a pas de diéren
e ave
les
autres langages de programmation : les fon
tions servent à dé
ouper le
ode de manière
logique, en éléments de taille raisonnable, et à éviter la dupli
ation de
ode.
1.3.1 Syntaxe
La syntaxe d'une dé
laration de fon
tion est
onforme à la syntaxe de son utilisation.
Ainsi
let f x = x * x
dénit une fon
tion f ayant un unique argument x et retournant son
arré. Comme on
le voit i
i, le
orps d'une fon
tion n'est autre qu'une expression, qui sera évaluée lorsque
la fon
tion sera appelée. Il n'y a pas de return
omme en C ou en Java,
e qui est une
fois en
ore
ohérent ave
l'absen
e de distin
tion expression/instru
tion. D'autre part, on
note que le système infère automatiquement le type de l'argument x, ainsi que le type du
résultat (et don
le type de la fon
tion f).
Si l'on entre la dé
laration
i-dessus dans le toplevel O
aml, on obtient
# let f x = x * x;;
val f : int -> int = <fun>
Autrement dit le système indique que l'on vient de dé
larer une variable appelée f, que
son type est int -> int
'est-à-dire le type d'une fon
tion prenant un entier en argument
et retournant un entier, et enn que sa valeur est <fun>
'est-à-dire une fon
tion. Une
telle valeur ne saurait être a
hée autrement,
ar le
ode n'est pas une valeur de première
lasse (
ontrairement à Lisp par exemple).
Comme nous l'avons déjà vu, l'appli
ation s'é
rit par simple juxtaposition. Ainsi l'ap-
pli
ation de f à 4 donne
# f 4;;
- : int = 16
11
Pro
édures et fon
tions sans argument
En O
aml, une pro
édure n'est rien d'autre qu'une fon
tion dont le résultat est de
type unit. On peut ainsi introduire une référen
e x et une fon
tion set pour en xer la
valeur :
# let x = ref 0;;
# let set v = x := v;;
val set : int -> unit = <fun>
et la fon
tion set ne retourne pas (vraiment) de valeur, ainsi que l'indique son type. Elle
pro
ède par eet de bord. On peut ainsi l'appeler sur la valeur 3 et
onstater l'eet sur
le
ontenu de la référen
e x :
# set 3;;
- : unit = ()
# !x;;
- : int = 3
Inversement une fon
tion sans argument va s'é
rire
omme prenant un unique argu-
ment de type unit,
elui-
i se notant alors (). On peut ainsi dénir une fon
tion reset
remettant le
ontenu de la référen
e x à 0 :
# let reset () = x := 0;;
val reset : unit -> unit = <fun>
et l'appel à reset s'é
rit reset (). Sur
e dernier point on
onstate que la syntaxe de
la valeur () n'a pas été
hoisie au hasard : on retrouve la syntaxe usuelle des fon
tions
C ou Java sans argument.
12
# let
arre x = x * x in
arre 3 +
arre 4 =
arre 5;;
- : bool = true
ou en
ore une fon
tion lo
ale à une autre fon
tion
# let pythagore x y z = let
arre n = n*n in
arre x +
arre y =
arre z;;
val pythagore : int -> int -> int -> bool = <fun>
On notera que la notion de fon
tion lo
ale existe dans
ertains langages tel que Pas
al
ou
ertaines extensions non-ANSI du C, même si la syntaxe y est un peu plus lourde qu'en
O
aml.
1.3.2 Fon
tions
omme valeurs de première
lasse
Jusqu'à présent, les fon
tions d'O
aml ne dièrent pas vraiment des fon
tions du C ou
des méthodes statiques de Java. Mais elles vont en réalité beau
oup plus loin, justiant le
nom de programmation fon
tionnelle. En eet, elles sont des valeurs de première
lasse,
'est-à-dire des valeurs pouvant être
réées par des
al
uls, passées en argument à des
fon
tions ou retournées,
omme n'importe quelles autres valeurs.
Une fon
tion peut être une expression
omme une autre, alors anonyme, et introduite
par le mot
lé fun. Ainsi
fun x -> x+1
est la fon
tion qui à un entier x asso
ie son su
esseur. C'est bien entendu une expression
ayant pour type int -> int et pouvant don
être appliquée à un entier :
# (fun x -> x+1) 3;;
- : int = 4
Une dé
laration de fon
tion de la forme
let f x = x + 1;;
n'est en réalité rien d'autre que du su
re syntaxique pour la dé
laration
let f = fun x -> x + 1;;
Il n'a don
pas une dé
laration let pour les variables (i
i au sens usuel de variable
ontenant une valeur d'un type de base tel que int) et une dé
laration let pour les
fon
tions, mais une unique dé
laration let pour introduire des variables pouvant
ontenir
des valeurs de type quel
onque, que
e soient des entiers, des booléens, des fon
tions, et
.
13
Cette dernière é
riture suggère que l'on peut appliquer une telle fon
tion à un seul ar-
gument. On peut ee
tivement le faire, et le résultat est alors une fon
tion. On parle
alors d'appli
ation partielle. Ainsi on peut dénir une fon
tion f prenant deux entiers en
arguments
# let f x y = x*x + y*y;;
val f : int -> int -> int = <fun>
puis
onstruire une se
onde fon
tion g en appliquant f partiellement :
# let g = f 3;;
val g : int -> int = <fun>
Le type de g est bien
elui d'une fon
tion prenant un argument entier, et si l'on applique
g à 4 on obtient bien la même
hose qu'en appliquant dire
tement f à 3 et 4 :
# g 4;;
- : int = 25
'est-à-dire que son
orps est le même que
elui de f dans lequel la variable formelle x a
été substituée par la valeur de l'argument ee
tif (3 i
i). Il est important de noter que
si l'on avait partiellement appliqué f à une expression plus
omplexe,
omme 1+2, alors
ette expression n'aurait été évaluée qu'une seule fois,
omme si l'on avait é
rit
let x = 1 + 2 in fun y -> x * x + y * y
et non pas substituée textuellement (
e qui équivaudrait alors à fun y -> (1+2)*(1+2)+y*y).
D'une manière générale, O
aml évalue toujours le ou les arguments d'une fon
tion avant
de pro
éder à l'appel : on dit que
'est un langage stri
t (par opposition aux langages dits
paresseux où l'évaluation d'un argument est retardée jusqu'au moment de sa première
utilisation).
L'appli
ation partielle d'une fon
tion est une expression qui est en
ore une fon
tion.
C'est don
une manière de retourner une fon
tion. Mais on peut pro
éder à un
al
ul
avant de retourner un résultat fon
tionnel,
omme dans
# let f x = let x2 = x * x in fun y -> x2 + y * y;;
val f : int -> int -> int = <fun>
On obtient i
i une fon
tion f prenant deux entiers et retournant un entier, qui se
omporte
omme si l'on avait é
rit
# let f x y = x * x + y * y;;
14
mais la première version est plus e
a
e si elle est appliquée partiellement. En eet, x
* x est alors
al
ulé une seule fois dans la variable x2, dont la valeur sera ensuite dire
-
tement
onsultée pour
haque appel à la fon
tion fun y -> x2 + y * y. Alors qu'une
appli
ation partielle de la se
onde version n'apportera au
un béné
e (si
e n'est peut-être
dans l'é
riture du
ode)
ar x * x sera re
al
ulé à
haque fois.
Un exemple subtil mais néanmoins typique est
elui d'un
ompteur utilisant une
référen
e. La fon
tion
ompteur_depuis
i-dessous prend en argument une valeur entière
et retourne un
ompteur (une fon
tion de type unit -> int produisant un nouvel entier
à
haque appel) démarrant à
ette valeur. Pour
ela une référen
e est
réée lo
alement à
la fon
tion servant de
ompteur :
# let
ompteur_depuis n = let r = ref (n-1) in fun () -> in
r r; !r;;
val
ompteur_depuis : int -> unit -> int = <fun>
(in
r est une fon
tion prédénie in
rémentant la valeur d'une référen
e entière). On
obtient alors un nouveau
ompteur
haque fois que l'on applique partiellement la fon
tion
ompteur_depuis :
# let
ompteur =
ompteur_depuis 0;;
val
ompteur : unit -> int = <fun>
#
ompteur ();;
- : int = 0
#
ompteur ();;
- : int = 1
#
ompteur ();;
- : int = 2
...
Ordre supérieur
De même qu'une fon
tion peut retourner une autre fon
tion
omme résultat, elle peut
également prendre une ou plusieurs fon
tions en argument. Cette
apa
ité de manipuler
les fon
tions
omme des valeurs de première
lasse est appelée ordre supérieur.
Ainsi on peut é
rire une fon
tion prenant en argument deux fon
tions f et g et re-
her
hant le premier entier naturel où elles dièrent d'au moins deux unités :
# let diff f g =
let n = ref 0 in while abs (f !n - g !n) < 1 do in
r n done; !n;;
val diff : (int -> int) -> (int -> int) -> int = <fun>
Le type est bien
elui d'une fon
tion prenant deux arguments,
ha
un de type int ->
int don
des fon
tions des entiers vers les entiers, et retournant un entier. On peut alors
appliquer diff à deux fon
tions :
# diff (fun x -> x) (fun x -> x*x);;
- : int = 2
Un exemple très
ourant de fon
tion d'ordre supérieur est
elui d'un itérateur. Dès
que l'on a une stru
ture de données de type
olle
tion (ensemble, di
tionnaire, le, et
.),
15
on l'équipe naturellement d'une manière d'itérer sur tous ses éléments (par exemple pour
les a
her ou les
ompter). En Java
ette itération se présente généralement sous la
forme d'une méthode elements retournant une énumération, elle-même équipée de deux
méthodes hasMoreElements et nextElement. L'itération est alors réalisée par une bou
le
de la forme
for (Enumeration e = v.elements() ; e.hasMoreElements() ;) {
... on traite e.nextElement() ...
}
En O
aml l'itérateur est habituellement réalisé par une fon
tion d'ordre supérieur pre-
nant en argument la fon
tion ee
tuant le traitement sur
haque élément. Ainsi une table
asso
iant des
haînes de
ara
tères à d'autres
haînes de
ara
tères fournira une fon
tion
de prol
val iter : (string -> string -> unit) -> table -> unit
Le premier argument de iter est la fon
tion qui sera appliquée à
haque
ouple de
haînes
présent dans la table, et le se
ond est la table proprement dite (d'un type table supposé
déni). Si l'on souhaite
ompter le nombre d'asso
iations dans une telle table t il sura
d'é
rire
let n = ref 0 in iter (fun x y -> in
r n) t; !n
et si l'on souhaite a
her toutes les asso
iations on pourra é
rire
iter (fun x y -> Printf.printf "%s -> %s\n" x y) t
La plupart des stru
tures de données O
aml fournissent de tels itérateurs, souvent ave
plusieurs variantes, y
ompris les stru
tures impératives usuelles telles que les tableaux,
les les, les tables de ha
hage, et
. Et pour
ette raison en parti
ulier il est fréquent
d'utiliser des fon
tions anonymes.
16
O
aml il faudrait
onstruire en C une stru
ture de données plus
omplexe,
ontenant non
seulement un pointeur de fon
tion mais également les valeurs sus
eptibles d'être utilisées
par
ette fon
tion. C'est
e que l'on appelle une
lture et
'est d'ailleurs ainsi qu'O
aml
représente les fon
tions (la référen
e à un mor
eau de
ode et un environnement dans
lequel évaluer
elui-
i).
Dans les langages de programmation impératifs, l'utilisation de fon
tions ré
ursives est
traditionnellement négligée, voire méprisée, au prot de bou
les. Il y a à
ela des raisons
historiques (le
oût d'un appel de fon
tion a longtemps été prohibitif) et des raisons
te
hniques (peu de langages
ompilent e
a
ement les fon
tions ré
ursives terminales,
d'où une utilisation ex
essive, voire fatale, de l'espa
e de pile).
Dans les langages fon
tionnels, l'utilisation de fon
tions ré
ursives est au
ontraire
privilégiée, pour les raisons inverses : d'une part un appel de fon
tion
oûte très peu
her, et d'autre part la ré
ursivité terminale est
orre
tement
ompilée en une bou
le. En
O
aml la dénition d'une fon
tion ré
ursive est introduite par l'adjon
tion du mot
lé
re
au mot
lé let.
Ainsi, une manière de réé
rire la fon
tion diff
i-dessus est d'utiliser une fon
tion
ré
ursive lo
ale au lieu d'une bou
le while :
let diff f g =
let re
bou
le n = if abs (f n - g n) < 1 then bou
le (n+1) else n in
bou
le 0
On voit tout de suite que
ette é
riture, à peine plus longue, évite l'utilisation d'une
référen
e. L'argument n de la fon
tion bou
le n'a en eet au
une raison d'être modié.
Cette version ré
ursive est même plus e
a
e que
elle utilisant une bou
le while (
ar
l'a
ès à n et son in
rémentation sont plus rapides que lorsqu'il s'agit d'une référen
e).
D'une manière générale, l'é
riture à l'aide d'une fon
tion ré
ursive donne souvent un
ode plus lisible et plus sus
eptible d'être
orre
t (
ar d'invariant plus simple) que son
équivalent impératif utilisant une bou
le. Pour s'en
onvain
re il sut de
omparer
es
deux versions de la fon
tion fa
torielle :
let re
fa
t n = if n = 0 then 1 else n * fa
t (n-1)
et
let fa
t n =
let f = ref 1 in
let i = ref n in
while !i > 0 do f := !f * !i; de
r i done;
!f
L'argument justiant la
orre
tion de la se
onde version est nettement plus
omplexe que
pour la première version.
17
1.3.4 Polymorphisme
18
Ré
apitulation
Jusqu'à présent nous avons manipulé des valeurs simples (entiers et booléens) et des
fon
tions. Dans
ette se
tion, nous allons aborder les types de données
omplexes tels que
tableaux, enregistrements, et
.,
'est-à-dire
eux qui impliquent une allo
ation mémoire1.
En O
aml, l'allo
ation mémoire est réalisée par un garbage
olle
tor ramasse-miettes
ou glaneur de
ellules en français, et GC pour faire
ourt. Le prin
ipal intérêt d'un GC
est la ré
upération automatique de la mémoire qui n'est plus utilisée. Les GC ne sont pas
propres aux langages de programmation fon
tionnelle ; Java en possède un également.
Outre la ré
upération automatique, l'intérêt d'un GC se situe également dans l'e
a
ité
de l'allo
ation mémoire. Celle-
i est bien supérieure à l'utilisation d'un mallo
en C. Le
GC d'O
aml est parti
ulièrement e
a
e, si bien qu'il faut perdre le réexe allouer
oûte
her qu'ont
ertains programmeurs, même s'il faut bien entendu
ontinuer à se
sou
ier de la
omplexité en espa
e des programmes que l'on é
rit.
Nous
ommençons par présenter les stru
tures usuelles de tableaux et d'enregistre-
ments, avant d'introduire l'un des points forts d'O
aml, les types
onstruits.
1.4.1 Tableaux
Mis à part une syntaxe un peu déroutante pour le débutant, les tableaux d'O
aml
sont très semblables aux tableaux de C ou de Pas
al. On alloue un tableau ave
la
fon
tion Array.
reate2 . Le premier argument est la taille du tableau et le se
ond la
valeur initiale de ses éléments :
# let a = Array.
reate 10 0;;
val a : int array = [|0; 0; 0; 0; 0; 0; 0; 0; 0; 0|℄
Le type int array est
elui d'un tableau dont les éléments sont de type int. Comme pour
une dé
laration de variable, le tableau doit être initialisé. Cela mérite une parenthèse. De
manière générale, O
aml n'autorise pas l'introdu
tion d'une valeur qui serait in
omplète
ou mal formée, et
e
i permet de garantir une propriété très forte : toute expression
bien typée s'évalue en une valeur de
e type, dès lors que l'évaluation termine et ne
lève pas d'ex
eption. Dit autrement, un programme O
aml ne peut pas produire de
segmentation fault
omme en C ou de NullPointerEx
eption
omme en Java.
1 Les valeurs fon
tionnelles impliquent également une allo
ation mémoire, mais nous avons passé
et
aspe
t sous silen
e.
2 Le point séparateur dans Array.
reate sera expliqué au
hapitre 2.
19
On remarque ensuite que le toplevel O
aml utilise une syntaxe parti
ulière pour
a
her la valeur du tableau. Cette syntaxe peut être utilisée en entrée pour allouer un
nouveau tableau :
let a = [| 1; 2; 3; 4 |℄
On a
ède à l'élément d'indi
e i du tableau a ave
la syntaxe a.(i) et on le modie (en
pla
e) ave
la syntaxe a.(i) <- v . On obtient la taille d'un tableau (en temps
onstant)
ave
la fon
tion Array.length. Voi
i par exemple
omment on é
rit le tri par insertion
en O
aml :
let tri_insertion a =
let swap i j = let t = a.(i) in a.(i) <- a.(j); a.(j) <- t in
for i = 1 to Array.length a - 1 do (* insérer l'élément i dans 0..i-1 *)
let j = ref (i - 1) in
while !j >= 0 && a.(!j) > a.(!j + 1) do swap !j (!j + 1); de
r j done
done
ou en
ore, en utilisant plutt une fon
tion ré
ursive pour la bou
le interne :
let tri_insertion a =
let swap i j = let t = a.(i) in a.(i) <- a.(j); a.(j) <- t in
for i = 1 to Array.length a - 1 do (* insérer l'élément i dans 0..i-1 *)
let re
insère j =
if j >= 0 && a.(j) > a.(j+1) then begin swap j (j+1); insère (j-1) end
in
insère (i-1)
done
1.4.2 Enregistrements
20
type personne = { nom : string; mutable age : int }
On peut alors modier le
hamp
orrespondant ave
l'opérateur <- :
# let p = { nom = "Martin"; age = 23 };;
val p : personne = {nom = "Martin"; age = 23}
# p.age <- p.age + 1;;
- : unit = ()
# p.age;;
- : int = 24
Création, a
ès et modi
ation d'une référen
e ne sont en réalité que des opérations
a
hées sur des enregistrements du type ref.
1.4.3 N-uplets
Il existe en O
aml une notion primitive de n-uplets. Un n-uplet est introduit ave
la
notation mathématique usuelle :
# (1,2,3);;
- : int * int * int = (1, 2, 3)
et son type est formé à partir de l'opérateur * et des types de ses diérents éléments. Le
n-uplet peut être d'arité quel
onque et ses éléments peuvent être de types diérents :
21
Une telle dé
laration let peut être globale (
omme i
i) ou lo
ale, et asso
ie à autant de
variables qu'il y a d'éléments dans le n-uplet les valeurs
orrespondantes (i
i a, b,
et
d). Les éléments d'un n-uplet ne sont pas modiables en pla
e et il n'y a pas moyen
de
hanger
et état de fait,
ontrairement aux enregistrements.
Les n-uplets sont utiles lorsqu'une fon
tion doit retourner plusieurs valeurs (on pour-
rait utiliser un type enregistrement mais un n-uplet est plus immédiat à utiliser). Ainsi
on peut é
rire une fon
tion de division par soustra
tion retournant quotient et reste sous
la forme d'une paire :
# let re
division n m =
if n < m then (0, n)
else let (q,r) = division (n - m) m in (q + 1, r);;
val division : int -> int -> int * int = <fun>
Note : Rien n'empê
he d'é
rire une fon
tion prenant plusieurs arguments sous la
forme d'une fon
tion prenant un unique n-uplet en argument. O
aml ore d'ailleurs une
syntaxe agréable pour
ela,
omparable à
elle du let destru
turant :
# let f (x,y) = x + y;;
val f : int * int -> int = <fun>
# f (1,2);;
- : int = 3
Si l'on retrouve alors la syntaxe usuelle des fon
tions C ou Java il ne faut pas s'y tromper :
d'une part, il s'agit bien d'une fon
tion O
aml prenant un unique argument, rendant en
parti
ulier l'appli
ation partielle impossible ; et d'autre part elle peut être moins e
a
e
qu'une fon
tion à plusieurs arguments, à
ause de la
onstru
tion d'un n-uplet lors de
son appel3 .
1.4.4 Listes
Il existe en O
aml un type prédéni de listes. Ces listes sont immuables (non mo-
diables en pla
e) et homogènes (tous les éléments d'une liste sont du même type). Si
α désigne le type des éléments d'une liste,
elle-
i a le type α list. On
onstruit des
listes à partir de la liste vide [℄ et de l'adjon
tion d'un élément en tête d'une liste ave
l'opérateur inxe ::. Ainsi la liste
ontenant les entiers 1, 2 et 3 peut être dénie par
# let l = 1 :: 2 :: 3 :: [℄;;
val l : int list = [1; 2; 3℄
Comme on le voit sur
et a
hage, O
aml propose une syntaxe plus
on
ise pour
onstruire dire
tement une liste étant donnés tous ses éléments :
# let l = [1; 2; 3℄;;
val l : int list = [1; 2; 3℄
3 En réalité, O
aml évite souvent de
onstruire un n-uplet lors de l'appel à une telle fon
tion.
22
De nombreuses fon
tions sur les listes sont prédénies : pour a
éder au premier élément,
à tous les autres,
al
uler la longueur, et
. La puissan
e des listes d'O
aml vient de la
possibilité de
onstru
tion par
as sur la forme d'une liste, appelée ltrage. Une liste est
en eet soit vide, soit formée d'un premier élément et d'une autre liste, et la
onstru
tion
mat
h with permet de raisonner par
as selon la forme d'une liste. On peut é
rire une
fon
tion
al
ulant la somme des éléments d'une liste d'entiers
omme
e
i :
# let re
somme l =
mat
h l with
[℄ -> 0
| x :: r -> x + somme r;;
val somme : int list -> int = <fun>
La
onstru
tion mat
h with est
onstituée d'une expression à examiner (entre les mots
lés mat
h et with, i
i la liste l) et d'un ou plusieurs
as de ltrage (séparés par une
barre verti
ale |). Un
as de ltrage est
onstitué d'un motif et d'une expression séparés
par une è
he. Le motif est un
onstru
teur (i
i [℄ ou ::) et ses arguments peuvent
être nommés (i
i les deux arguments de :: sont nommés x et r). La sémantique est
intuitive : l'expression examinée est
omparée aux diérents motifs (selon leur ordre
d'apparition) et lorsqu'il y a
orrespondan
e pour un motif, l'expression asso
iée à
e
motif est évaluée, dans un environnement où les variables du motif sont asso
iées aux
valeurs
orrespondantes dans l'expression ltrée.
Ainsi, si l'on applique la fon
tion somme à la liste 1::2::3::[℄, le
as de ltrage qui
s'applique est le se
ond, x prenant la valeur 1 et r la valeur 2::3::[℄. On évalue alors
l'expression x+somme r dans
et environnement. Ce
as de ltrage va en
ore s'appliquer
deux fois, avant que l'on parvienne nalement à la liste [℄ pour laquelle le premier
as
s'appliquera, retournant la valeur 0. On aura au nal le résultat attendu :
# somme [1;2;3℄;;
- : int = 6
On
omprend vite d'où vient la puissan
e du ltrage : il agit
omme une série de
tests et de dénitions de variables lo
ales en même temps, le tout ave
une syntaxe
extrêmement
on
ise. Il existe même un ra
our
i syntaxique pour faire du ltrage sur
le dernier argument d'une fon
tion, ave
le mot
lé fun
tion. On peut ainsi réé
rire la
fon
tion somme aussi simplement que
let re
somme = fun
tion
| [℄ -> 0
| x :: r -> x + somme r
(Il est possible d'introduire une barre verti
ale avant le premier
as de ltrage, dans un
sou
i purement esthétique de symétrie.)
Il est très important de
omprendre que les listes d'O
aml ne sont fondamentalement
pas diérentes des listes
haînées que l'on utiliserait en C ou en Java. La liste vide est
représentée en interne par l'entier 0 (
omme le pointeur null en C ou en Java) et une liste
non vide est représentée par un pointeur vers un blo
mémoire
ontenant deux valeurs,
à savoir l'élément de la liste et le reste de la liste (à son tour soit 0 pour [℄, soit un
pointeur). La liste [1 ;2 ;3℄
orrespond don
à une allo
ation mémoire de la forme :
23
1
3 [℄
Lorsqu'un programme O
aml manipule des listes (les passer en arguments à des fon
-
tions, les retourner, et
.), il ne fait que manipuler des pointeurs, exa
tement
omme le
ferait un bon programme C ou Java. Mais la diéren
e essentielle est qu'O
aml ne per-
met de
onstruire que des listes bien formées, en parti
ulier par
e que les pointeurs ne
sont pas expli
ités. Et là où un programmeur C ou Java doit penser à tester si un poin-
teur est null, le programmeur O
aml utilisera une
onstru
tion de ltrage qui l'obligera
à
onsidérer
e
as, mais ave
une grande
on
ision syntaxique.
Les listes ne sont qu'un
as parti
ulier de types
onstruits (en
ore appelés types
algébriques ). Un type
onstruit regroupe des valeurs formées à partir d'un ou plusieurs
onstru
teurs. Comme pour les enregistrements, les types
onstruits doivent être dé
larés,
an d'introduire les noms des
onstru
teurs et leur arité. Ainsi la dé
laration suivante
type formule = Vrai | Faux | Conjon
tion of formule * formule
introduit un nouveau type
onstruit formule, ayant deux
onstru
teurs
onstants (Vrai
et Faux) et un
onstru
teur Conjon
tion ayant deux arguments de type formule. Les
onstru
teurs
onstants sont dire
tement des valeurs du type
onstruit :
# Vrai;;
- : formule = Vrai
Les
onstru
teurs non
onstants doivent être appliqués à des arguments de types
ompa-
tibles ave
leur dé
laration :
# Conjon
tion (Vrai, Faux);;
- : formule = Conjon
tion (Vrai, Faux)
Les parenthèses sont obligatoires. Un
onstru
teur est né
essairement
omplètement ap-
pliqué ;
e n'est pas une fon
tion.
Le type prédéni des listes a la dénition suivante, à la syntaxe de ses
onstru
teurs
prêt :
type 'a list = [℄ | :: of 'a * 'a list
On peut voir un type
onstruit grossièrement
omme une union de stru
tures en C, mais
ave
une représentation mémoire plus e
a
e. En eet,
haque
onstru
teur est représenté
soit par un entier s'il est
onstant (
omme la liste vide), soit par un pointeur vers un blo
mémoire de la taille adéquate sinon.
La notion de ltrage introduite ave
les listes se généralise à tout type
onstruit.
Ainsi on peut é
rire une fon
tion d'évaluation des formules logiques
i-dessus de la façon
suivante :
24
# let re
evalue = fun
tion
| Vrai -> true
| Faux -> false
| Conjon
tion (f1, f2) -> evalue f1 && evalue f2;;
val evalue : formule -> bool = <fun>
Le ltrage sur les types
onstruits peut être imbriqué, i.e. les arguments des
onstru
teurs
dans les motifs peuvent être à leur tour des motifs. Ainsi on peut raner la fon
tion
evalue en
let re
evalue = fun
tion
| Vrai -> true
| Faux -> false
| Conjon
tion (Faux, f2) -> false
| Conjon
tion (f1, Faux) -> false
| Conjon
tion (f1, f2) -> evalue f1 && evalue f2;;
On a multiplié les motifs pour faire apparaître des
as parti
uliers (lorsqu'un argument
de Conjon
tion est Faux on retourne false dire
tement). Le
ompilateur O
aml vérie
d'une part l'exhaustivité du ltrage (tous les
as sont
ouverts), et d'autre part l'absen
e
de motif
ouvert par un motif pré
édent ; il émet un avertissement en
as de problème.
Lorsqu'un argument de
onstru
teur n'a pas besoin d'être nommé, il peut être rem-
pla
é par _ (le motif universel). Lorsque deux motifs sont asso
iés à la même expression,
ils peuvent être regroupés par une barre verti
ale. Ainsi la fon
tion
i-dessus devient-elle
en
ore plus lisible :
let re
evalue = fun
tion
| Vrai -> true
| Faux -> false
| Conjon
tion (Faux, _) | Conjon
tion (_, Faux) -> false
| Conjon
tion (f1, f2) -> evalue f1 && evalue f2;;
Le ltrage n'est pas limité aux types
onstruits. Il peut être utilisé sur des valeurs
de tout type, ave
la syntaxe habituelle. Ainsi on peut multiplier les éléments d'une liste
d'entiers en
ombinant ltrage sur les listes et sur les entiers :
let re
mult = fun
tion
| [℄ -> 1
| 0 :: _ -> 0
| x :: l -> x * mult l
Enn, lorsque le ltrage est
omposé d'un seul motif, il peut être é
rit de manière
plus
on
ise à l'aide d'une
onstru
tion let, sous la forme let motif = expr . La syntaxe
utilisée plus haut pour déstru
turer les n-uplets n'en est qu'un
as parti
ulier.
Ré apitulation
25
les valeurs allouées sont né
essairement initialisées ;
la majorité des valeurs
onstruites ne sont pas modiables en pla
e : seuls le sont
les tableaux et les
hamps d'enregistrements expli
itement dé
larés mutable ;
la représentation mémoire des valeurs
onstruites est e
a
e ;
le ltrage permet un examen par
as sur les valeurs
onstruites.
1.5 Ex eptions
Il nous reste un aspe
t du langage de base à dé
rire : les ex
eptions. Elles sont
omparables aux ex
eptions qui existent dans d'autres langages tels que Java. À tout
instant une ex
eption peut être levée pour signaler un
omportement ex
eptionnel, une
erreur le plus souvent. Une ex
eption est levée à l'aide de la
onstru
tion raise :
let division n m =
if m = 0 then raise Division_by_zero else ...
La
onstru
tion raise est une expression
omme une autre, mais son typage est parti
u-
lier : elle peut prendre n'importe quel type, selon son
ontexte d'utilisation. Ainsi dans
l'expression
if x >= 0 then 2 * x else raise Negatif
l'expression raise Negatif a le type int, de manière à
e que l'expression
omplète soit
bien typée, alors que dans l'expression
if x < 0 then raise Negatif; 2 * x
la même expression raise Negatif aura le type unit.
On rattrape les ex
eptions à l'aide de la
onstru
tion try with dont la syntaxe est
identique (au mot
lé près) à
elle de la
onstru
tion mat
h with. Ainsi on peut é
rire
try division x y with Division_by_zero -> (0,0)
L'intégralité de la
onstru
tion try with est une expression O
aml, i
i de type int *
int. L'évaluation de
ette expression se fait ainsi : l'expression division x y est évaluée ;
si elle donne un résultat,
elui-
i est retourné
omme le résultat de l'expression toute
entière ; si l'ex
eption Division_by_zero est levée, alors l'expression (0,0) est évaluée
et son résultat est
elui de l'expression toute entière ; si une autre ex
eption est levée, elle
est propagée jusqu'au premier try with à même de la rattraper.
On peut dé
larer de nouvelles ex
eptions ave
la dé
laration ex
eption et les ex
ep-
tions,
omme les
onstru
teurs, peuvent avoir des arguments (non polymorphes) :
ex
eption Error
ex
eption Unix_error of string
En réalité, les ex
eptions sont des
onstru
teurs, du type prédéni exn. Comme en Java
les ex
eptions sont des valeurs de première
lasse. On peut ainsi é
rire
# let vérifie e = if e = Not_found then raise e;;
val vérifie : exn -> unit = <fun>
26
Il existe quelques ex
eptions prédénies, dont la plus
ourante est Not_found. Elle
est utilisée notamment dans les stru
tures de données implantant des tables d'asso
ia-
tion, pour signaler l'absen
e de valeur asso
iée. Ainsi la fon
tion de re
her
he dans une
table de ha
hage, Hashtbl.find, lève l'ex
eption Not_found pour signaler une re
her
he
infru
tueuse. On ren
ontrera don
souvent la stru
ture de
ode
try Hashtbl.find table
lé
with Not_found -> ...
Si les ex
eptions servent prin
ipalement à la gestion des erreurs, elles peuvent égale-
ment être utilisées à d'autres ns, par exemple pour modier le ot de
ontrle. On peut
ainsi utiliser une ex
eption pour sortir d'une bou
le,
omme dans
try
while true do
let key = read_key () in
if key = 'q' then raise Exit;
...
done
with Exit ->
lose_graph (); exit 0
27
Chapitre 2
Modules
Lorsque les programmes deviennent gros, il est important qu'un langage de program-
mation apporte de bons outils de génie logi
iel, en parti
ulier pour dé
ouper les pro-
grammes en unités de taille raisonnable (modularité ), o
ulter la représentation
on
rète
de
ertaines données (en
apsulation ), et éviter au mieux la dupli
ation du
ode. Il existe
de nombreuses manières de parvenir à
es obje
tifs. Dans la programmation orientée
objets,
e sont les
lasses et les
on
epts asso
iés qui en sont la
lé. En O
aml,
es
fon
tionnalités sont apportées par le système de modules.
2.1 Modules
Comme le nom de module le suggère, un module est avant tout une manière d'in-
troduire de la modularité dans un programme, i.e. de le dé
ouper en unités de taille
raisonnable.
L'unité de programme la plus simple est le
hier. En O
aml,
haque
hier
onstitue
un module diérent. Si l'on pla
e un
ertain nombre de dé
larations dans un
hier, disons
arith.ml :
let pi = 3.141592
let round x = floor (x +. 0.5)
alors la
ompilation de
e
hier en tant que module se fait en invoquant une
ompilation
sans édition de lien grâ
e à l'option -
du
ompilateur (même option que g
) :
% o
aml
-
arith.ml
28
On peut alors faire référen
e aux éléments de
e module dans un autre
hier O
aml,
par l'intermédiaire de la notation Arith.x. Ainsi on peut utiliser le module Arith dans
un autre
hier main.ml :
let x = float_of_string (read_line ());;
print_float (Arith.round (x /. Arith.pi));;
print_newline ();;
et ensuite
ompiler
e se
ond
hier :
% o
aml
-
main.ml
Pour que
ette
ompilation réussisse, il est né
essaire que le module Arith ait été préa-
lablement
ompilé : en eet, lorsqu'il est fait référen
e à un élément de module,
omme
i
i Arith.pi, le
ompilateur re
her
he un
hier d'interfa
e arith.
mi. On peut alors
réaliser l'édition de liens, en fournissant les deux
hiers de
ode au
ompilateur O
aml :
% o
aml
arith.
mo main.
mo
2.1.2 En apsulation
Les modules en tant que
hiers permettent don
le dé
oupage du
ode. Mais les
modules permettent bien d'autres
hoses, dont l'en
apsulation, i.e. la possibilité d'o
ulter
ertains détails de
odage. On a en eet la possibilité de fournir une interfa
e aux modules
que l'on dénit, et seuls les éléments présents dans
ette interfa
e seront visibles, de même
qu'en Java le mot
lé private limite la visibilité d'attributs ou de méthodes. Pour
ela on
pla
e l'interfa
e dans un
hier de suxe .mli à
té du
hier de suxe .ml
ontenant
le
ode. Ainsi on peut n'exporter que la fon
tion round du module Arith
i-dessus en
réant un
hier arith.mli de
ontenu :
val round : float -> float
On
onstate que la syntaxe est la même que
elle utilisée par le toplevel O
aml dans ses
réponses. Ce
hier d'interfa
e doit être
ompilé avant le
hier de
ode
orrespondant :
% o
aml
-
arith.mli
% o
aml
-
arith.ml
29
Lors de la
ompilation du
ode, le
ompilateur vérie l'adéquation de types entre les
éléments dé
larés dans l'interfa
e et les éléments ee
tivement dénis dans le
ode. Si
l'on
her
he à re
ompiler le module main.ml on obtient maintenant une erreur :
% o
aml
-
main.ml
File "main.ml", line 2,
hara
ters 33-41:
Unbound value Arith.pi
En revan
he la dénition de pi reste visible dans l'intégralité du
hier arith.ml.
L'interfa
e ne se limite pas à restreindre les valeurs exportées. Elle permet également
de restreindre les types exportés et mieux en
ore leur seule dénition. Supposons par
exemple que l'on souhaite
oder une mini-bibliothèque d'ensembles d'entiers représentés
par des listes. Le
ode pourrait
onstituer le
hier ensemble.ml suivant
type t = int list
let vide = [℄
let ajoute x l = x :: l
let appartient = List.mem
et une interfa
e possible serait la suivante :
type t = int list
val vide : t
val ajoute : int -> t -> t
val appartient : int -> t -> bool
I
i la dénition du type t ne sert à rien, sinon à améliorer la le
ture de l'interfa
e en iden-
tiant les ensembles (sans le type t, le
ompilateur aurait inféré des types plus généraux
mais
ompatibles pour vide, ajoute et appartient).
Mais on peut aussi
a
her la représentation du type t, en donnant à l'interfa
e le
ontenu suivant :
type t
val vide : t
val ajoute : int -> t -> t
val appartient : int -> t -> bool
Un tel type s'appelle un type abstrait ;
'est une notion essentielle sur laquelle nous re-
viendrons au
hapitre 3. Faire du type t
i-dessus un type abstrait
a
he
omplètement
la manière dont les valeurs de
e type sont formées. Si l'on
her
he à traiter une valeur du
type Ensemble.t
omme une liste d'entiers (en dehors du
hier ensemble.ml), on ob-
tient une erreur. On peut don
modier à loisir le
odage de
es ensembles sans perturber
le reste du programme.
Compilation séparée
Le langage O
aml permet en eet la
ompilation séparée. Cela signie que la
om-
pilation d'un
hier ne dépend que des interfa
es des modules qu'il mentionne, et non
de leur
ode. Ainsi si un module B utilise un autre module A, une modi
ation dans le
30
ode de A n'implique pas de re
ompiler B. Seule une modi
ation de l'interfa
e de A le
justierait,
ar il faut alors vérier que les
hangements éventuels des types des valeurs
exportées par A préservent le typage de B.
La
ompilation séparée ore un gain de temps
onsidérable au programmeur. Sur un
projet
omposé de
ent
hiers O
aml, une modi
ation de
ode n'impliquera qu'une
seule
ompilation et une édition de liens pour refaire l'exé
utable.
L'une des for
es du système de modules d'O
aml réside dans le fait qu'il ne se limite
pas aux
hiers. On peut en eet dénir un nouveau module de même que l'on dénit un
type, une
onstante, une fon
tion ou une ex
eption. Ainsi on peut é
rire
module M = stru
t
let
= 100
let f x =
* x
end
Le mot
lé module introduit i
i la dé
laration d'un nouveau module, M, dont la dénition
est le blo
en
adré par stru
t et end. Un tel blo
est aussi appelé une stru
ture (d'où le
mot
lé stru
t) et est
omposé d'une suite de dé
larations et/ou expressions, exa
tement
omme un texte de programme. De telles dé
larations de modules peuvent être mêlées
aux autres dé
larations, et
ontenir elles-mêmes d'autres dé
larations de modules. On
peut ainsi é
rire des
hoses
omme :
module A = stru
t
let a = 2
module B = stru
t
let b = 3
let f x = a * b * x
end
let f x = B.f (x + 1)
end
De tels modules lo
aux prennent tout leur sens ave
les fon
teurs, qui sont ex-
posés dans la se
tion suivante, mais on
onstate déjà qu'ils permettent une organisation
de l'espa
e de nommage. Ainsi dans l'exemple
i-dessus, on a pu dénir deux fon
tions
appelées f,
ar
elle dénie à l'intérieur du module B n'est pas immédiatement visible
dans le
orps du module A ; il faut qualier ave
la notation B.f pour y avoir a
ès. On
peut don
se servir des modules lo
aux pour regrouper un
ertain nombre de types et
fon
tions ayant un rapport entre eux, et leur donner des noms génériques (
omme add,
find, et
.)
ar la quali
ation par le nom du module permet de les distinguer d'autres
fon
tions de mêmes noms.
Lorsque l'on fait très souvent référen
e à des valeurs d'un module, on peut éviter la
quali
ation systématique par le nom du module en ouvrant le module ave
la dé
laration
open. Ce
i a pour eet de rendre visibles tous les éléments de
e module. Ainsi, plutt
que d'é
rire systématiquement Printf.printf, on ouvrira le module Printf en début
de
hier :
31
open Printf
...
let print x y = printf "%d + %d = %d\n" x y (x+y)
...
Signatures
À l'instar de toutes les autres dé
larations, les dé
larations de modules sont typées.
Le type d'un module est appelé une signature. La syntaxe d'une signature est identique
à
elles des interfa
es (les
hiers .mli) et en
adrée par les mots
lés sig end. On le
onstate en dé
larant un module dans le toplevel :
# module M = stru
t let a = 2 let f x = a * x end;;
module M : sig val a : int val f : int -> int end
Il est logique que la syntaxe soit identique à
elle d'une interfa
e
ar une interfa
e n'est
rien d'autre que le type d'un module-
hier. De même que l'on peut dénir des modules
lo
aux, on peut dénir des signatures :
# module type S = sig val f : int -> int end;;
module type S = sig val f : int -> int end
On peut alors se servir de telles signatures pour typer expli
itement des dé
larations de
modules :
# module M : S = stru
t let a = 2 let f x = a * x end;;
module M : S
L'intérêt d'une telle dé
laration est le même que
elui des interfa
es : restreindre les
valeurs ou types exportées (
e que nous avons appelé l'en
apsulation). I
i la valeur M.a
n'est plus visible :
# M.a;;
Unbound value M.a
Ré apitulation
De même qu'une fon
tion O
aml peut être générique vis-à-vis de types (polymor-
phisme) ou d'autres fon
tions (ordre supérieur), un module peut être paramétré par un
ou plusieurs autres modules. Un tel module s'appelle un fon
teur.
32
Pour bien justier l'utilité des fon
teurs, le plus simple est de prendre un exemple.
Supposons que l'on veuille
oder une table de ha
hage en O
aml, de la façon la plus
générique possible de manière à
e que
e
ode soit réutilisable dans toute
ir
onstan
e où
une table de ha
hage est requise. Pour
ela il faut paramétrer le
ode vis-à-vis d'une fon
-
tion de ha
hage (pour l'insertion dans la table et la re
her
he) et d'une fon
tion d'égalité
(pour la re
her
he)1 . On pourrait imaginer utiliser l'ordre supérieur et paramétrer toutes
les fon
tions de notre module par
es deux fon
tions,
e qui donnerait une interfa
e de la
forme
type 'a t
(* le type abstrait des tables de ha
hage
ontenant des éléments
de type "'a" *)
val
reate : int -> 'a t
(* "
reate n"
rée une nouvelle table de taille initiale "n" *)
val add : ('a -> int) -> 'a t -> 'a -> unit
(* "add h t x" ajoute la donnée "x" dans la table "t" à l'aide
de la fon
tion de ha
hage "h" *)
val mem : ('a -> int) -> ('a -> 'a -> bool) -> 'a t -> 'a -> bool
(* "mem h eq t x" teste l'o
urren
e de "x" dans la table "t" pour
la fon
tion de ha
hage "h" et l'égalité "eq" *)
Au delà de sa lourdeur, une telle interfa
e est dangereuse : en eet, on peut ajouter
un élément dans la table ave
une
ertaine fon
tion de ha
hage, puis le re
her
her ave
une autre fon
tion de ha
hage. Le résultat sera alors erroné, mais le système de types
d'O
aml n'aura pas permis d'ex
lure
ette utilisation in
orre
te (elle reste bien typée).
Une solution un peu plus satisfaisante
onsisterait à xer les fon
tions de ha
hage
et d'égalité à la
réation de la table. Elles seraient alors sto
kées dans la stru
ture de
données et utilisées par les opérations
haque fois que né
essaire. L'interfa
e deviendrait
alors plus raisonnable :
type 'a t
(* le type abstrait des tables de ha
hage
ontenant des éléments
de type "'a" *)
val
reate : ('a -> int) -> ('a -> 'a -> bool) -> int -> 'a t
(* "
reate h eq n"
rée une nouvelle table de taille initiale "n"
ave
"h" pour fon
tion de ha
hage et "eq" pour égalité *)
val add : 'a t -> 'a -> unit
(* "add t x" ajoute la donnée "x" dans la table "t" *)
val mem : 'a t -> 'a -> bool
(* "mem t x" teste l'o
urren
e de "x" dans la table "t" *)
De manière interne, le
odage pourrait ressembler à quelque
hose
omme
type 'a t = { hash : 'a -> int; eq : 'a -> 'a -> bool; data : 'a list array }
let
reate h eq n = { hash = h; eq = eq; data = Array.
reate n [℄ }
...
1 Ilse trouve qu'O
aml prédénit une égalité et une fon
tion de ha
hage polymorphes, mais il arrive
que l'on souhaite utiliser des fon
tions diérentes.
33
Cependant, un tel
odage a en
ore des défauts : d'une part, l'a
ès aux fon
tions de
ha
hage et d'égalité se fait for
ément par l'intermédiaire de
ltures et pénalise l'exé
ution
par rapport à des fon
tions statiquement
onnues ; d'autre part, la stru
ture de données
odant la table
ontient maintenant des fon
tions et pour
ette raison il devient di
ile
de l'é
rire sur le disque et de la relire ultérieurement (
'est te
hniquement possible mais
ave
d'importantes restri
tions).
Heureusement, les fon
teurs apportent i
i une solution très satisfaisante. Puisque l'on
souhaite paramétrer le
odage de nos tables de ha
hage par un type (le type des éléments)
et deux fon
tions, on va é
rire un module paramétré par un autre module
ontenant
es
trois éléments. Un tel fon
teur F paramétré par un module X de signature S s'introduit
ave
la syntaxe
module F(X : S) = stru
t ... end
Dans notre
as la signature S est la suivante
module type S = sig
type elt
val hash : elt -> int
val eq : elt -> elt -> bool
end
34
On peut alors instan
ier le fon
teur F sur le module de son
hoix. Ainsi, si l'on souhaite
une table de ha
hage pour sto
ker des entiers on pourra
ommen
er par dénir un module
Entiers ayant la signature S :
Appli
ations
Les appli
ations des fon
teurs sont multiples. On les utilise en parti
ulier pour dénir
1. des stru
tures de données paramétrées par d'autres stru
tures de données
O
aml ore trois tels fon
teurs dans sa bibliothèque standard :
Hashtbl.Make : des tables de ha
hage semblables à
elles que nous venons de
voir ;
Set.Make : des ensembles nis
odés par des arbres équilibrés ;
Map.Make : des tables d'asso
iations
odées par des arbres équilibrés.
2. des algorithmes paramétrés par des stru
tures de données
Ainsi on peut é
rire l'algorithme de Dijkstra de re
her
he du plus
ourt
hemin
dans un graphe sous forme d'un fon
teur paramétré par la stru
ture de donnée
représentant le graphe. Le type d'un tel fon
teur pourrait être
module Dijkstra
(G : sig
type graph (* type des graphes *)
type sommet (* type des sommets *)
35
val voisins : graph -> sommet -> (sommet * int) list
(* ensemble des voisins ave
le poids de l'arête *)
end) :
sig
val plus_
ourt_
hemin :
G.graph -> G.sommet -> G.sommet -> G.sommet list * int
(* "plus_
ourt_
hemin g a b" re
her
he le plus
ourt
hemin de
"a" à "b" dans "g" *)
end
On voit don
que les fon
teurs sont un moyen puissant de réutiliser le
ode, en l'é
ri-
vant de la manière la plus générique possible. Les fon
teurs peuvent être rappro
hés des
templates de C++, bien que les diéren
es soient nombreuses.
Note : Le système de modules d'O
aml est en réalité indépendant du langage de
base. Il forme un langage à part entière, qui pourrait être appliqué à tout autre langage.
En parti
ulier, il pourrait être
omplètement déplié statiquement pour donner un
ode
sans module ni fon
teur, même si
e n'est pas
e qui est fait en pratique.
36
Chapitre 3
Persistan
e
Dans
e
hapitre nous allons revenir sur un aspe
t essentiel des stru
tures de données,
la persistan
e. Cette notion n'est pas propre à O
aml, ni même à la programmation
fon
tionnelle, mais est de fait naturelle dans
ette famille de langages.
Comme nous l'avons fait remarquer à de nombreuses reprises, les stru
tures de données
O
aml sont pour l'essentiel immuables
'est-à-dire non modiables une fois
onstruites.
Ce
i a une
onséquen
e très importante : une valeur d'un tel type n'est pas ae
tée par
une opération sur
ette valeur ; seules de nouvelles valeurs sont retournées. Le plus simple
est de l'illustrer ave
des listes.
Si on dénit la liste l par let l = [1 ; 2 ; 3℄ alors l est, en termes de représentation
mémoire, un pointeur vers un premier blo
ontenant 1 et un pointeur vers un se
ond
blo
, et
. :
l 1 2 3 ⊥
Nous l'avons déjà expliqué. Si on dénit maintenant la liste l'
omme l'adjon
tion d'un
autre élément à la liste l, ave
la dé
laration let l' = 0 :: l, on a maintenant la
situation suivante :
l
l' 0 1 2 3 ⊥
'est-à-dire que l'appli
ation du
onstru
teur :: a eu pour eet d'allouer un nouveau
blo
, dont le premier élément est 0 et le se
ond un pointeur ayant la même valeur que
l. La variable l
ontinue de pointer sur les mêmes blo
s qu'auparavant. D'une manière
générale, n'importe quelle fon
tion que l'on pourra é
rire sur les listes aura
ette propriété
de ne pas modier les listes qui lui sont passées en arguments. C'est
ette propriété de la
stru
ture de données que l'on appelle persistan
e.
Il est très important de
omprendre qu'il y a i
i partage. La dé
laration de l' n'alloue
pas plus qu'un seul blo
(puisqu'un seul
onstru
teur est appliqué), les blo
s formant
37
l étant en quelque sorte réutilisés mais non modiés. On a bien deux listes de 3 et 4
éléments respe
tivement, à savoir [1 ;2 ;3℄ et [0 ;1 ;2 ;3℄, mais seulement quatre blo
s
mémoire. En parti
ulier il n'y a pas eu de
opie. D'une manière générale, O
aml ne
opie
jamais de valeurs, sauf si l'on é
rit expli
itement une fon
tion de
opie, telle que
let re
opie = fun
tion [℄ -> [℄ | x :: l -> x ::
opie l
mais une telle fon
tion est
omplètement inutile
ar une liste ne peut être modiée en
pla
e. Les fon
tions de
opie ne sont utiles que lorsque les stru
tures de données sont
sus
eptibles d'être modiées en pla
e.
On
omprend maintenant qu'il n'y a pas de possibilité d'ajouter un élément en queue
de liste aussi fa
ilement qu'en tête,
ar
ela signierait une modi
ation en pla
e de la
liste l :
l 1 2 3 0 ⊥
Pour rajouter un élément en queue de liste, il faut
opier tous les blo
s de la liste. C'est
e que fait en parti
ulier la fon
tion append suivante qui
onstruit la
on
aténation de
deux listes :
let re
append l1 l2 = mat
h l1 with
| [℄ -> l2
| x :: l -> x :: append l l2
On
onstate que
ette fon
tion re
rée autant de blo
s qu'il y en a dans l1, pour ne
partager que
eux de l2. Ainsi, si l'on dé
lare let l' = [4 ; 5℄ puis que l'on réalise la
on
aténation de l et de l' ave
let l = append l l', on aura la situation suivante :
l 1 2 3 ⊥ l' 4 5 ⊥
l 1 2 3
Les blo
s de l ont été
opiés et
eux de l' partagés.
Pour
ette raison les listes doivent être utilisées lorsque les opérations naturelles sont
l'ajout et le retrait en tête (stru
ture de pile ). Lorsque les a
ès et/ou modi
ations
doivent se faire à des positions arbitraires, il vaut mieux préférer une autre stru
ture de
données.
Note importante : les éléments de la liste eux-mêmes, en revan
he, ne sont pas
opiés
par la fon
tion append. En eet, x désigne un élément de type quel
onque et au
une
opie n'est ee
tuée sur x lui-même. Sur des listes d'entiers,
e n'était pas signi
atif.
Mais si l'on a une liste l
ontenant trois éléments d'un type plus
omplexe, par exemple
des triplets d'entiers
omme dans let l =[(1,2,3) ; (4,5,6) ; (7,8,9)℄, alors
eux-
i
resteront partagés entre l et append l [(10,11,12)℄ :
l ⊥ l' ⊥
1 2 3 4 5 6 7 8 9 10 11 12
38
Tout
e
i peut paraître inutilement
oûteux lorsque l'on a l'habitude d'utiliser des
listes modiées en pla
e,
e qui est la manière traditionnelle de faire dans le
ontexte de
langages impératifs. Mais
e serait sous-estimer l'intérêt pratique de la persistan
e. Il est
d'ailleurs important de noter que le
on
ept de persistan
e peut être fa
ilement mis en
÷uvre dans un langage impératif : il sut de manipuler les listes
haînées exa
tement
omme le
ompilateur O
aml le fait. Inversement, on peut tout à fait manipuler des
listes modiables en pla
e en O
aml, par exemple en dénissant le type suivant :
type 'a liste = Vide | Element of 'a element
and 'a element = { valeur : 'a; mutable suivant : 'a liste };;
(On peut même rendre le
hamp valeur mutable si on le souhaite.) Mais à l'inverse
des langages impératifs, O
aml ore la possibilité de dénir des stru
tures de données
immuables de manière naturelle et sûre (
ar même si l'on
ode une stru
ture persistante
en C ou Java, le système de types ne peut empê
her sa modi
ation en pla
e, les variables
étant par nature modiables).
Enn, il ne faut pas oublier que la mémoire inutilisée est automatiquement ré
upérée
par le GC d'O
aml. Ainsi dans une expression telle que
let l = [1;2;3℄ in append l [4;5;6℄
les trois blo
s de l sont ee
tivement
opiés lors de la
onstru
tion de la liste [1 ;2 ;3 ;4 ;5 ;6℄
mais immédiatement ré
upérés par le GC
ar nulle part référen
és.
39
On suppose qu'une fon
tion booléenne sortie indique si un état
orrespond à la sortie.
On é
rit alors trivialement la fon
tion
her
he sous la forme suivante :
let re
her
he e =
sortie e || essaye e (dépla
ements e)
and essaye e = fun
tion
| [℄ -> false
| d :: r ->
her
he (dépla
e d e) || essaye e r
où essaye est une fon
tion testant un par un les dépla
ements possibles d'une liste de
dépla
ements. C'est la persistan
e de la stru
ture de données
odant les états qui permet
une telle
on
ision de
ode. En eet, si l'état devait être modié en pla
e il faudrait
ee
tuer le dépla
ement avant d'appeler ré
ursivement
her
he dans essaye mais aussi
annuler
e dépla
ement en
as d'é
he
avant de passer aux autres dépla
ements possibles.
Le
ode ressemblerait alors à quelque
hose
omme :
let re
her
he () =
sortie () || essaye (dépla
ements ())
and essaye = fun
tion
| [℄ -> false
| d :: r -> (dépla
e d;
her
he ()) || (revient d; essaye r)
e qui est indubitablement moins
lair et plus propi
e aux erreurs. Cet exemple n'est
pas arti
iel : le ba
ktra
king est une te
hnique
ouramment utilisée en informatique
(par
ours de graphes,
oloriage, dénombrement de solutions, et
.)
Un autre exemple typique est
elui de la portée dans les manipulations symboliques
de programmes (ou de formules). Supposons par exemple que l'on manipule des pro-
grammes Java triviaux
omposés uniquement de blo
s, de variables lo
ales entières, de
tests d'égalité entre deux variables et de return,
'est-à-dire des programmes
omme :
{
int i = 0;
int j = 1;
if (i == j) {
int k = 0;
if (k == i) { return j; } else { int i = 2; return i; }
} else {
int k = 2;
if (k == j) { int j = 3; return j; } else { return k; }
}
}
De tels programmes peuvent être représentés en O
aml par le type instr list où le
type
onstruit instr est déni par
type instr =
| Return of string
| Var of string * int
| If of string * string * instr list * instr list
40
Supposons maintenant que l'on souhaite vérier que les variables manipulées dans de tels
programmes ont bien toujours été dé
larées au préalable. Pour
ela on peut é
rire deux
fon
tions mutuellement ré
ursives vérifie_instr et vérifie_prog vériant respe
tive-
ment qu'une instru
tion et qu'une liste d'instru
tions sont bien formées. Pour
ela
es
deux fon
tions prennent en argument la liste des variables dans la portée desquelles on
se trouve (i.e. visibles). Le
ode est presque immédiat :
let re
vérifie_instr vars = fun
tion
| Return x ->
List.mem x vars
| If (x, y, p1, p2) ->
List.mem x vars && List.mem y vars &&
vérifie_prog vars p1 && vérifie_prog vars p2
| Var _ ->
true
and vérifie_prog vars = fun
tion
| [℄ ->
true
| Var (x, _) :: p ->
vérifie_prog (x :: vars) p
| i :: p ->
vérifie_instr vars i && vérifie_prog vars p
La simpli
ité de
e
ode tient à l'utilisation d'une stru
ture de données persistante pour
l'ensemble des variables, à savoir une liste. Si l'on avait utilisé une table impérative pour
ela, il aurait fallu revenir en arrière entre la première bran
he d'un if et la se
onde, de
manière à oublier les variables lo
ales à la première bran
he. Le
ode n'aurait pas été
aussi simple.
Cet exemple est relativement simple, mais on en trouve beau
oup d'instan
es en
pratique, dès que l'on fait des manipulations symboliques et que des phénomènes de
portées interviennent (table de symboles, environnement de typage, et
.). Il devient alors
très agréable d'utiliser une stru
ture de données persistante.
Donnons un dernier exemple de l'utilité de la persistan
e. Supposons un programme
manipulant une base de données. Il n'y a qu'une seule instan
e de
ette base à
haque
instant et don
a priori il n'y a pas lieu d'utiliser une stru
ture persistante pour
ette
base. Mais supposons que les mises à jour ee
tuées dans
ette base soient
omplexes, i.e.
impliquent
ha
une un grand nombre d'opérations dont
ertaines peuvent é
houer. On se
retrouve alors dans une situation di
ile où il faut savoir annuler les eets du début de
la mise à jour. S
hématiquement, le
ode pourrait ressembler à
e
i :
try
... effe
tuer l'opération de mise à jour ...
with e ->
... rétablir la base dans un état
ohérent ...
... traiter ensuite l'erreur ...
Si l'on utilise une stru
ture persistante pour la base de données, il sut de sto
ker la
base dans une référen
e, soit bd, et l'opération de mise à jour devient une mise à jour de
ette référen
e :
41
let bd = ref (... base initiale ...)
...
try
bd := (... opération de mise à jour de !bd ...)
with e ->
... traiter l'erreur ...
Dès lors, il n'y a pas lieu d'annuler quoi que
e soit. En eet, l'opération de mise à jour,
si
omplexe qu'elle soit, ne fait que
onstruire une nouvelle base de données et une fois
seulement
ette
onstru
tion terminée, la référen
e bd est modiée pour pointer sur
ette
nouvelle base. Cette toute dernière modi
ation est atomique et ne peut é
houer. S'il y a
une quel
onque ex
eption levée pendant l'opération de mise à jour proprement dite, alors
la référen
e bd restera in
hangée.
Le type de données des listes est persistant d'une manière évidente,
ar
'est un
type
onstruit dont on
onnaît la dénition, i.e.
on
ret et immuable. Lorsqu'un module
O
aml implante une stru
ture de données sous la forme d'un type abstrait, son
ara
tère
persistant ou non n'est pas immédiat. Bien entendu, un
ommentaire approprié dans
l'interfa
e peut renseigner le programmeur sur
et état de fait. Mais en pratique
e sont les
types des opérations qui fournissent
ette information. Prenons l'exemple d'une stru
ture
de données persistante représentant des ensembles nis d'entiers. L'interfa
e d'un tel
module ressemblera à
e
i :
type t (* le type abstrait des ensembles *)
val empty : t (* l'ensemble vide *)
val add : int -> t -> t (* l'ajout d'un élément *)
val remove : int -> t -> t (* la suppression d'un élément *)
...
Le
ara
tère persistant des ensembles est impli
ite dans l'interfa
e. En eet, l'opération
add retourne un élément de type t, i.e. un nouvel ensemble ; de même pour la suppression.
Plus subtilement, l'ensemble vide empty est une valeur et non une fon
tion, et toutes les
o
urren
es de empty seront don
partagées quelque soit sa représentation.
En revan
he une stru
ture de données modiable en pla
e pour des ensembles d'entiers
(par exemple sous la forme de tables de ha
hage) présentera une interfa
e de la forme :
type t (* le type abstrait des ensembles *)
val
reate : unit -> t (* 'ensemble vide *)
val add : int -> t -> unit (* l'ajout d'un élément *)
val remove : int -> t -> unit (* la suppression d'un élément *)
...
I
i la fon
tion d'ajout add ne retourne rien,
ar elle a ajouté l'élément en pla
e dans
la stru
ture de données, et il en sera de même pour les autres opérations. D'autre part
la fon
tion
reate prend un argument de type unit
ar
haque appel à
reate doit
42
onstruire une nouvelle instan
e de la stru
ture de données, an que les modi
ations en
pla
e sur l'une n'ae
te pas l'autre.
Malheureusement, rien n'empê
he de donner à une stru
ture impérative une interfa
e
persistante (en retournant la valeur passée en argument) et inversement de donner
à une stru
ture persistante une interfa
e impérative (en jetant les valeurs que l'on
vient de
onstruire). Dans les deux
as,
'est stupide et dangereux.
Cela ne signie pas pour autant qu'une stru
ture de données persistante est né
essai-
rement
odée sans au
un eet de bord. La bonne dénition de persistant est
persistant = observationnellement immuable
et non purement fon
tionnel (au sens de l'absen
e d'eet de bord). On a seulement
l'impli
ation dans un sens :
purement fon
tionnel ⇒ persistant
La ré
iproque est fausse, à savoir qu'il existe des stru
tures de données persistantes faisant
usage d'eets de bord. Un exemple
lassique est
elui d'arbres binaires de re
her
he
évoluant au fur et à mesure des re
her
hes pour optimiser l'a
ès aux éléments les plus
re
her
hés (Splay trees ). Ce
i peut être réalisé en
odant la stru
ture de données
omme
une référen
e sur un arbre, lui-même
odé par une stru
ture persistante. L'arbre optimisé
peut alors être substitué à l'arbre
ourant par modi
ation de
ette référen
e, mais la
stru
ture
omplète reste persistante (pas de modi
ation observable du
ontenu, mais
seulement des performan
es).
Un autre exemple plus simple est
elui des les (rst-in rst-out ou queue en anglais).
Si l'on
her
he à en réaliser une version persistante à l'aide de listes, les performan
es ne
sont pas bonnes,
ar ajouter un élément en tête de liste est immédiat mais supprimer un
élément en queue de liste ne l'est pas (le
oût est proportionnel au nombre d'éléments
en temps et en espa
e). Une idée astu
ieuse
onsiste à représenter la le par deux listes,
l'une re
evant les éléments en entrée et l'autre
ontenant les éléments prêts à sortir (don
rangés dans l'autre sens). Les opérations d'ajout et de retrait ont don
toujours un
oût
O(1) sauf dans le
as parti
ulier où la liste de sortie se trouve être vide ; on retourne alors
la liste d'entrée pour l'utiliser
omme nouvelle liste de sortie, la liste d'entrée devenant
vide. Le
oût de
e retournement est proportionnel à la longueur de la liste retournée,
mais
e
i ne sera fait qu'une fois pour l'ensemble des éléments de
ette liste. La
omplexité
amortie (i.e. ramenée à l'ensemble des opérations d'ajout et de retrait) reste don
O(1).
Ave
l'interfa
e minimale suivante pour des les persistantes :
type 'a t
val
reate : unit -> 'a t
val push : 'a -> 'a t -> 'a t
ex
eption Empty
val pop : 'a t -> 'a * 'a t
on peut proposer le
ode suivant basé sur l'idée
i-dessus :
type 'a t = 'a list * 'a list
let
reate () = [℄, [℄
43
let push x (e,s) = (x :: e, s)
ex
eption Empty
let pop = fun
tion
| e, x :: s -> x, (e,s)
| e, [℄ -> mat
h List.rev e with
| x :: s -> x, ([℄, s)
| [℄ -> raise Empty
Mais il se peut tout de même que l'on soit amené à retourner plusieurs fois la liste
d'entrée e, si l'opération pop est ee
tuée plusieurs fois sur une même le de la forme
(e,[℄). Après tout,
ela est sus
eptible d'arriver puisque l'on s'évertue à
onstruire des
les persistantes.
Pour remédier à
ela, on va enregistrer le retournement de la liste d'entrée e par un
eet de bord dans la stru
ture de données. Cela n'en ae
tera pas le
ontenu la le
(e,[℄)
ontient exa
tement les mêmes éléments que la le ([℄,List.rev e) et évitera
d'avoir à retourner la même liste la fois suivante. On rempla
e don
la paire de liste par
un enregistrement
omposé de deux
hamps entrée et sortie tous deux mutable :
type 'a t = { mutable entrée : 'a list; mutable sortie : 'a list }
let
reate () = { entrée = [℄; sortie = [℄ }
let push x q = { entrée = x :: q.entrée; sortie = q.sortie }
ex
eption Empty
et le
ode de la fon
tion pop est à peine plus
omplexe :
let pop q = mat
h q.sortie with
| x :: s ->
x, { entrée = q.entrée; sortie = s }
| [℄ -> mat
h List.rev q.entrée with
| [℄ ->
raise Empty
| x :: s as r ->
q.entrée <- [℄; q.sortie <- r;
x, { entrée = [℄; sortie = s }
La seule diéren
e tient dans les deux modi
ations en pla
e des
hamps de la le q.
(Notez l'utilisation du mot
lé as dans le ltrage qui permet de nommer l'ensemble de la
valeur ltrée an d'éviter de la re
onstruire.)
44