Vous êtes sur la page 1sur 44

Université Paris Sud

Master Informatique M1 20052006

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

LRI bâtiment 490


Université Paris Sud
91405 ORSAY Cedex
Fran e

Tél : 01-69-15-65-76
Bureau 144
Email : filliatrlri.fr
Web : http://www.lri.fr/~filliatr/

4
Chapitre 1
Fondamentaux

Le terme de programmation fon tionnelle désigne une famille de langages de program-


mation ayant un ertain nombre de traits en ommun, dont un rle entral donné aux
fon tions, d'où le nom. Les représentants les plus onnus de ette famille se nomment
Haskell, Standard ML et O aml, parmi de nombreux autres.
La programmation fon tionnelle est très souvent opposée à la programmation impé-
rative, famille regroupant des langages tels que C ou Pas al mais aussi Java ou C++,
où l'on pro ède essentiellement par eets de bord i.e. modi ation en pla e du ontenu
des variables. Mais ette opposition n'a pas vraiment lieu d'être. En eet, seul le langage
Haskell ex lut omplètement les eets de bord et est dit pour ela purement fon tionnel.
Les langages tels que Standard ML ou O aml autorise parfaitement les eets de bords
et omportent d'ailleurs toutes les stru tures usuelles de la programmation impérative
(variables modiables en pla e, tableaux, bou les for et while, entrées-sorties, et .)
Malheureusement, omme nous le verrons à plusieurs reprises, le terme fon tionnel
évoque souvent l'absen e d'eets de bord ou le ara tère immuable, omme dans l'expres-
sion malheureuse  stru ture de données (purement) fon tionnelle . Mais en réalité, les
langages de programmation fon tionnels partagent beau oup de points ave eux dits im-
pératifs. Et ils ont également beau oup de points forts en dehors du ara tère immuable
de ertaines de leurs valeurs ou de leur gestion des fon tions. C'est e que nous allons
essayer d'illustrer dans e ours.
Dans e qui suit, il nous arrivera parfois de faire un parallèle ave un mor eau de
ode C ou Java. Que le le teur ne onnaissant pas l'un ou l'autre ne s'inquiète pas et se
ontente de l'ignorer.

1.1 Premiers pas

1.1.1 Le premier programme

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 ?

1.1.5 Variables lo ales

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

Pour résumer rapidement ette se tion, nous avons vu


 qu'un programme est une suite d'expressions et de dé larations ;
 que les variables introduites par le mot lé let ne sont pas modiables ;
 qu'il n'y a pas de distin tion entre expressions et instru tions.

1.2 La bou le d'intera tion o aml


Avant d'aller plus loin, nous pouvons nous arrêter sur une spé i ité du langage
O aml : sa bou le d'intera tion. À té du ompilateur o aml dont nous avons déjà
illustré l'usage, il existe un autre  ompilateur  du langage, intera tif elui- i. On le
lan e ave la ommande o aml :
% o aml
Obje tive Caml version 3.08.0

#
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.

1.3 Fon tions

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.

Fon tions à plusieurs arguments


Une fon tion ayant plusieurs arguments se dé lare ave toujours la même syntaxe
onsistant à juxtaposer fon tion et arguments :
# let f x y z = if x > 0 then y + x else z - x;;
val f : int -> int -> int -> int = <fun>
On voit sur et exemple que le type d'une fon tion prenant trois entiers en argument et re-
tournant un entier s'é rit int -> int -> int -> int. L'appli ation d'une telle fon tion
emprunte toujours la même syntaxe :
# f 1 2 3;;
- : int = 3

Fon tions lo ales


Comme nous le verrons plus en détail dans la se tion suivante, une fon tion est en
O aml une valeur omme une autre. Dès lors elle peut être dé larée lo alement omme
une variable de n'importe quel autre type. Ainsi on peut é rire

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 .

Appli ation partielle


Les fon tions anonymes peuvent avoir plusieurs arguments, ave la syntaxe suivante
fun x y -> x * x + y * y
De manière rigoureusement équivalente on peut é rire
fun x -> fun y -> x * x + y * y

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

La fon tion g est omparable à la fon tion


fun y -> 3 * 3 + y * y

'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.

Diéren e ave les pointeurs de fon tions


Il est très important de bien omprendre la diéren e ave les pointeurs de fon tions
du C, ar l'on entend bien souvent  en C aussi une fon tion peut re evoir ou retourner
une fon tion par l'intermédiaire d'un pointeur de fon tion .
Pour une ertaine atégorie de fon tions d'ordre supérieur susamment simples il n'y
a en eet pas de diéren e. Mais dès lors que les fon tions onstruites font référen e à
des al uls lo aux il n'y a plus d'analogie possible. Reprenons l'exemple de la fon tion
let f x = let x2 = x * x in fun y -> x2 + y * y;;
Lorsque l'on applique partiellement f on obtient une fon tion qui fait référen e à la
valeur de x2. Plus pré isément, haque appli ation partielle donnera une nouvelle fon tion
faisant référen e à une variable x2 diérente. On ne saurait réaliser la même hose ave
des pointeurs de fon tions, ar la fon tion C en question devrait faire référen e à des
variables lo ales qui ne survivent pas à l'appel à f (de manière générale, il est in orre t
de retourner un pointeur sur une fon tion C lo ale). Pour obtenir le même eet qu'en

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).

1.3.3 Fon tions ré ursives

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

Au point où nous en sommes, le le teur attentif peut s'être légitimement demandé si


une dé laration telle que
let f x = x;;
est a eptée par le ompilateur O aml et le as é héant quel est le type donné à ette
fon tion. Il s'avère qu'une telle dé laration est en eet a eptée et qu'O aml lui donne
le type suivant :
# let f x = x;;
val f : 'a -> 'a = <fun>
I i 'a ne désigne pas vraiment un type mais une variable de type, pouvant prendre n'im-
porte quelle valeur parmi tous les types possibles. Le type de f est don elui d'une
fon tion prenant un argument d'un type quel onque et retournant une valeur du même
type. Une telle fon tion est dite polymorphe. On peut ainsi l'appliquer à un entier :
# f 3;;
- : int = 3
mais aussi à un booléen :
# f true;;
- : bool = true
ou en ore à une fon tion :
# f print_int;;
- : int -> unit = <fun>
Un autre exemple de fon tion polymorphe est elui d'une fon tion hoisissant une
valeur parmi deux en fon tion d'un troisième argument booléen :
# let hoix b x y = if b then x else y;;
val hoix : bool -> 'a -> 'a -> 'a = <fun>
ou en ore l'exemple typique de la fon tion réalisant la omposition de deux fon tions :
# let ompose f g = fun x -> f (g x);;
val ompose : ('a -> 'b) -> (' -> 'a) -> ' -> 'b = <fun>
Sur e dernier exemple on onstate qu'O aml infère bien le type attendu : la omposition
d'une fon tion de A vers B et d'une fon tion de C vers A est une fon tion de C vers B .
D'une manière générale, O aml infère toujours le type le plus général possible. D'autre
part, on remarque que l'on aurait pu aussi bien é rire :
let ompose f g x = f (g x);;
La vision naturelle de ompose omme prenant deux fon tions et retournant une fon tion
n'est qu'une manière de voir ette fon tion, omme partiellement appliquée. Mais on peut
très bien la voir omme prenant trois arguments f, g et x et retournant f (g x).

18
Ré apitulation

Dans ette se tion, nous avons vu que


 les fon tions sont des valeurs omme les autres : elles peuvent être lo ales, anonymes,
arguments d'autres fon tions, et . ;
 les fon tions peuvent être partiellement appliquées ;
 les fon tions peuvent être polymorphes ;
 l'appel de fon tion ne oûte pas her.

1.4 Allo ation mémoire

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

Les enregistrements en O aml sont omparables aux re ords du Pas al ou aux


stru tures du C. Comme dans es langages-là, il faut ommen er par dé larer le type
enregistrement et ses hamps :
type omplexe = { re : float; im : float }
On peut alors dénir un enregistrement ave la syntaxe suivante :
# let x = { re = 1.0; im = -1.0 };;
val x : omplexe = {re = 1.; im = -1.}
Comme toujours, on note que l'enregistrement doit être totalement initialisé, et que son
type est inféré (i i le type omplexe). On a ède au hamp d'un enregistrement ave la
notation usuelle :
# x.im;;
- : float = -1.
En revan he, les hamps d'un enregistrement ne sont pas modiables par défaut. Pour
ela il faut le dé larer expli itement à l'aide du mot lé mutable :

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

Retour sur les référen es


On peut maintenant expliquer e que sont les référen es d'O aml. Ce ne sont rien
d'autre que des enregistrements du type polymorphe prédéni suivant :
type 'a ref = { mutable ontents : 'a }
'est-à-dire un enregistrement ave un unique hamp mutable appelé ontents. On s'en
aperçoit en inspe tant la valeur d'une référen e dans le toplevel :
# ref 1;;
- : int ref = { ontents = 1}

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 :

# let v = (1, true, "bonjour", 'a');;


val v : int * bool * string * har = (1, true, "bonjour", 'a')
On peut a éder aux diérents éléments d'un n-uplet à l'aide d'une dé laration let
déstru turante :
# let (a,b, ,d) = v;;
val a : int = 1
val b : bool = true
val : string = "bonjour"
val d : har = 'a'

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.

1.4.5 Types onstruits

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

Dans ette se tion, nous avons vu que


 l'allo ation mémoire ne oûte pas her, la libération se fait automatiquement ;

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.

2.1.1 Fi hiers et modules

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

Le résultat de ette ompilation est omposé de deux  hiers : arith. mo ontenant le


ode et arith. mi ontenant son interfa e (i i une onstant pi de type float et une
fon tion round de type float -> float). Le nom du module onstitué par un  hier
est obtenu en apitalisant la première lettre de son nom (si né essaire). I i le module
s'appelle don Arith.

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

Remarque : on aurait pu également ompiler main.ml et réaliser l'édition de liens en une


seule ommande :
% o aml arith. mo main.ml
ou même ompiler les deux  hiers et réaliser l'édition de liens en une unique ommande :
% o aml arith.ml main.ml
Dans e dernier as, les  hiers sont ompilés dans l'ordre de leur o urren e sur la ligne
de ommande, e qui a l'eet es ompté.

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.

2.1.3 Langage de modules

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

Pour résumer, le système de modules d'O aml permet


 la modularité, par le dé oupage du ode en unités appelées modules ;
 l'en apsulation de types et de valeurs, et en parti ulier la dénition de types abs-
traits ;
 une vraie ompilation séparée ;
 une organisation de l'espa e de nommage.

2.2 Fon teurs

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

et le odage des tables de ha hage peut don s'é rire ainsi :


module F(X : S) = stru t
type t = X.elt list array
let reate n = Array. reate n [℄
let add t x =
let i = (X.hash x) mod (Array.length t) in t.(i) <- x :: t.(i)
let mem t x =
let i = (X.hash x) mod (Array.length t) in List.exists (X.eq x) t.(i)
end
On onstate que dans le orps du fon teur, on a ède aux éléments du module-paramètre
X exa tement omme s'il s'agissait d'un module déni plus haut. Le type du fon teur F
expli ite le paramètre X ave la même syntaxe :
module F(X : S) : sig
type t
(* le type abstrait des tables de ha hage ontenant des éléments
de type "X.elt" *)
val reate : int -> t
(* " reate h eq n" rée une nouvelle table de taille initiale "n" *)
val add : t -> X.elt -> unit
(* "add t x" ajoute la donnée "x" dans la table "t" *)
val mem : t -> X.elt -> bool
(* "mem t x" teste l'o urren e de "x" dans la table "t" *)
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 :

module Entiers = stru t


type elt = int
let hash x = abs x
let eq x y = x=y
end
puis appliquer le fon teur F sur e module :
module Hentiers = F(Entiers)
On onstate don que la dénition d'un module n'est pas limitée à une stru ture : il peut
s'agir aussi d'une appli ation de fon teur. On peut alors utiliser le module Hentiers
omme tout autre module :
# let t = Hentiers. reate 17;;
val t : Hentiers.t = <abstr>
# Hentiers.add t 13;;
- : unit = ()
# Hentiers.add t 173;;
- : unit = ()
# Hentiers.mem t 13;;
- : bool = true
# Hentiers.mem t 14;;
- : bool = false
...

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.

3.1 Stru tures de données immuables

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.

3.2 Intérêts pratiques de la persistan e

Les intérêts pratiques de la persistan e sont multiples. De manière immédiate, on


omprend que la persistan e fa ilite la le ture du ode et sa orre tion. En eet, on peut
alors raisonner sur les valeurs manipulées par le programme en termes  mathématiques ,
puisqu'elles sont immuables, de manière équationnelle et sans même se sou ier de l'ordre
d'évaluation. Ainsi est-il fa ile de se persuader de la orre tion de la fon tion append
i-dessus une fois que l'on a énon é e qu'elle est ensé faire (i.e. append l1 l2 onstruit
la liste formée des éléments de l1 suivis des éléments de l2) : une simple ré urren e sur la
stru ture de l1 sut. Ave des listes modiables en pla e et une fon tion append allant
modier le dernier pointeur de l1 pour le faire pointer sur l2 l'argument de orre tion
est nettement plus di ile. L'exemple est en ore plus agrant ave le retournement d'une
liste.
La orre tion d'un programme n'est pas un aspe t négligeable et doit toujours l'em-
porter sur son e a ité : qui se sou ie en eet d'un programme rapide mais in orre t ?
La persistan e n'est pas seulement utile pour augmenter la orre tion des programmes,
elle est également un outil puissant dans les ontextes où le ba ktra king (retour en ar-
rière) est né essaire. Supposons par exemple que l'on é rive un programme her hant la
sortie dans un labyrinthe, sous la forme d'une fon tion her he prenant en argument un
état, persistant, et retournant un booléen indiquant une re her he réussie. Les dépla e-
ments possibles à partir d'un état sont donnés sous forme d'une liste par une fon tion
dépla ements et une autre fon tion dépla e al ule le résultat d'un dépla ement à partir
d'un état, sous la forme d'un nouvel état puisqu'il s'agit d'un type de données persistant.

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.

3.3 Interfa e et persistan 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

Vous aimerez peut-être aussi