Caml
Caml
fonctionnel
Caml
Jacques Le Maitre
Table des matières
1 Introduction ........................................................................................................................... 1
Le langage Caml (il faut prononcer « Camel ») est un langage de programmation fonctionnel
de la famille ML. ML a été inventé par l’équipe de Robert Milner à Edimbourg. C’était à
l’origine un métalangage (d’où son nom) pour un système de vérification de preuves. C’est
devenu ensuite un langage de programmation complet.
Plusieurs versions de ML sont maintenant disponibles, dont les plus connues sont Standard
ML et Caml. Caml a été conçu à l’INRIA (Institut National de Recherche en Informatique et
Automatique). Deux versions de Caml existent actuellement : Caml Light et Objective Caml.
Caml Light est bien adapté à l’apprentissage de la programmation. Objective Caml, qui
intègre Caml Light, permet la manipulation d’objets et dispose d’un compilateur très
performant. Ces deux langages tournent sous UNIX, Windows et MacOS. C’est Caml Light
qui est utilisé comme support de ce cours. C’est un logiciel libre qui peut être téléchargé à
partir du site Web de de l’INRIA ([Link]
Les principales caractéristiques de Caml, sont les suivantes :
– Caml est un langage fonctionnel. Les fonctions sont des valeurs à part entière qui peuvent
être argument ou valeur d’une fonction.
– Caml possède une syntaxe conviviale.
– Caml est un langage typé. Les types des variables ne sont pas déclarés par le programmeur
mais calculés automatiquement par l’interpréte Caml.
– Caml supporte le filtrage, le polymorphisme et le traitement des exceptions.
– Caml permet la programmation impérative.
Un programme Caml manipule des valeurs. Une valeur peut être une valeur de base (un
nombre, un booléen, un caractère, une chaîne de caractères), une valeur structurée (un n-
uplet, une liste ou un enregistrement) ou bien une fonction. Toute valeur a un type. Un
programme Caml se présente comme une suite de phrases qui sont soit des expressions à
évaluer, soit des définitions. Une expression est construite à partir : de constantes littérales
représentant des valeurs de base, de noms de valeurs, de constructeurs de valeurs structurées
et de l’opération d’application d’une fonction. La valeur d’une expression est calculée en
effectuant toutes les applications qu’elle contient. On dit que l’expression a été évaluée,
réduite ou simplifiée. Une définition consiste à donner un nom à la valeur d’une expression.
Voici, par exemple, un programme Caml qui définit la longueur et la largeur d’un rectangle
puis en calcule le périmètre.
#let lon = 15;;
#let lar = 5;;
#2 * (lon + lar);;
La valeur de ce périmètre est obtenue par les applications successives des fonctions
prédéfinies + et *.
Avant de commencer l’étude de Caml voici quelques informations sur son utilisation en mode
interactif. Le caractère d’invite (« prompt » en anglais) est le caractère #. A son invite, le
programmeur peut entrer une phrase qui peut tenir sur plusieurs lignes et doit se terminer par
;;. La phrase est validée par un retour chariot. Caml affichera alors :
– soit un message d’erreur,
– soit une réponse constituée en général d’une valeur et de son type.
Par exemple :
#(3 + 7) * 5;;
- : int = 50
L’expression (3 + 7) * 5 est anonyme (-) de type int (entier) et a pour valeur 50.
Trois types d’unités lexicales apparaissent dans un programme Caml : des identificateurs, des
mots-clés réservés et des symboles spéciaux. Un identificateur est un mot composé d’une
lettre suivie ou non d’une suite de caractères dont chacun peut être une lettre, un chiffre, le
caractère _ ou le caractère '. Par exemple :
l1 les_villes x'1
Les symboles spéciaux sont utilisés comme séparateurs ou bien pour désigner les opérateurs
les plus courants (+ ou <=, par exemple). Les mots-clés réservés sont des identificateurs qui
jouent un rôle particulier dans le langage (if, then ou else, par exemple).
Plusieurs livres sur Caml ont été écrits. Ce cours est fortement inspiré de trois d’entre eux :
– Pierre Weis, Xavier Leroy, Le langage Caml, InterEditions.
– Xavier Leroy, Pierre Weis, Manuel de référence du langage Caml, InterEditions.
– Thérèse Accart Hardin, Véronique Donzeau-Gouge Viguié, Concepts et outils de
programmation. Le style fonctionnel, le style impératif avec Caml et Ada, InterEditions.
Les deux premiers livres ont été écrits par les concepteurs de Caml et de Caml Light. Le
premier, remarquablement écrit, est plus qu’un cours sur Caml : c’est un véritable cours de
programmation. Dans sa deuxième partie il présente un choix d’exemples très complets qui
démontrent l’expressivité et la puissance de Caml. Le troisième livre est issu d’un cours de
premier cycle du CNAM. L’idée était d’introduire la programmation fonctionnelle puis la
programmation impérative à partir des langages Caml et Ada. La première partie du livre est
consacrée à Caml dont la sémantique est présentée de façon très claire. On trouvera de plus,
dans ce livre, un grand nombre d’exercices corrigés.
Ce chapitre introduit les concepts de base du langage Caml : les valeurs manipulées ; les
expressions, qui sont des calculs soumis à l’interprète Caml ; la définition de valeurs, qui
consiste à leur donner un nom ; l’environnement qui mémorise les liaisons entre noms et
valeurs et auquel Caml se réfère lors d’un calcul et enfin le fonctionnement de l’interprète lui-
même.
2.1 Valeurs
Caml manipule des valeurs qui sont classées par types. Un type représente un ensemble de
valeurs. Toute valeur appartient à un et un seul type. Il faut faire une distinction entre
l’expression d’un type et son extension. L’expression d’un type exprime la façon dont il est
construit à partir d’autres types. L’extension d’un type est l’ensemble des valeurs de ce type.
Si t est une expression de type nous noterons ext(t) l’extension de t.
Les types constructibles en Caml seront étudiés pas à pas tout au long de ce cours. Pour
débuter nous considérerons un sous-ensemble des types de Caml, que nous appellerons types
de base. Ce sont les suivants :
– Le type bool dont l’extension est constitué des valeurs true et false (ext(bool) = {true,
false}).
– Le type int dont l’extension est l’ensemble des entiers appartenant à un intervalle
dépendant de l’implémentation ([-230, 230-1] en général).
– Le type float dont l’extension est l’ensemble des flottants appartenant à un intervalle
dépendant de l’implémentation (si possible des flottants double précision à la norme
IEEE).
– Le type char dont l’extension est l’ensemble des caractères dont le code numérique est
compris entre 0 et 255. Les caractères dont le code est compris entre 0 et 127 sont du
standard ASCII et ceux dont le code est compris entre 128 et 255 dépendent de
l’implémentation.
– Le type string dont l’extension est l’ensemble des chaînes de 0 à n caractères où n dépend
de l’implémentation (65535 au minimum).
– Le type unit dont l’extension ne contient qu’une seule valeur notée () et qui signifie
« rien ».
2.2 Environnements
Une valeur peut être nommée. Un nom de valeur est :
– soit un identificateur différent des mots-clés réservés. Par exemple :
Les noms de valeurs sont aussi appelés variables. Ils doivent être différent des mots-clés
réservés de Caml comme cela est classique dans la plupart des langages de programmation.
Une liaison est une association entre un nom et une valeur. Une liaison entre un nom n et
une valeur v de type t sera notée :
n:t=v
Par exemple :
lon : int = 15
Un environnement est un ensemble de liaisons. Par exemple :
{lon : int = 15, lar : int = 5}
Au début d’une session Caml il existe un environnement initial qui contient des liaisons
prédéfinies, nous l’appellerons Env_init.
Un environnement peut être étendu par un autre environnement. L’extension d’un
environnement Env1 par un environnement Env2, que nous noterons Env1 Env2, est obtenue
en ajoutant les liaisons de Env2 aux liaisons de Env1 dont le nom n’est pas présent dans Env2.
On a :
Env1 Env2 = {n : t = v | n Env1 et n Env2 } Env2
On constate que cette opération à pour effet de ne garder que les liaisons les plus récentes de
chaque nom. Par exemple :
{lon : int = 10} {lon : int = 15, lar : int = 5} = {lon : int = 15, lar : int = 5}
2.3 Expressions
Une expression est une construction Caml destinée à être évaluée. Toute expression a un type
et une valeur. Le type d’une expression est déterminé (on dit aussi synthétisé) par Caml avant
son évaluation.
Le type et la valeur d’une expression Caml dépendent de l’environnement dans lequel elle est
évaluée. Si e est une expression et Env un environnement nous noterons
type(e, Env)
et
val(e, Env)
le type et la valeur de e dans Env. Par la suite nous dirons qu’une expression e est valide dans
un environnement Env, si type(e, Env) est calculable.
Les types et les expressions valides en Caml seront étudiés pas à pas tout au long de ce cours.
Pour débuter nous considérerons un sous-ensemble d’expressions (voir Figure 1.1), que nous
appellerons de base, qui sont construites à partir des valeurs, des noms de valeur, et
d’opérateurs sur les entiers, les flottants et les chaînes de caractères.
Considérons, par exemple, l’environnement :
Env = {lon : int = 15, lar : int = 5}
On a :
type(2, Env) = int
type(lon, Env) = int
type(lar, Env) = int
type(lon + lar, Env) = int
type(2 * (lon + lar), Env) = int
et :
val(2 * (lon + lar), Env)
= val(2, Env) * val((lon + lar), Env)
= 2 * val(lon + lar, Env)
= 2 * (val(lon, Env) + val(lar, Env))
= 2 * (15 + 5)
= 40
2.4 Alternative
Il est souvent nécessaire d’effectuer des calculs qui dépendent d’une condition. Pour cela
Caml dispose d’une construction particulière : l’alternative. Une expression alternative a la
forme suivante :
if e1 then e2 else e3
où e1 est une expression de type bool et e2 et e3 sont des expressions de même type. Par
exemple :
#if 10 > 5 then "Evidemment !" else "Bizarre !";;
- : string = "Evidemment !"
2.5 Définitions
où n1, …, nk sont des noms de valeurs et e1, …, ek des expressions. Elle a pour effet, pour tout i
de 1 à n, de donner le nom ni à la valeur de l’expression ei. Cette valeur constitue la définition
de ni. Par exemple :
#let deux_fois_pi = 2. *. 3.14;;
deux_fois_pi : float = 6.28
#let lon = 15 and lar = 5;;
lon : int = 15
lar : int = 5
La définition de deux_fois_pi est 6.28, celle de lon est 15 et celle de lar est 5.
Une définition globale a pour effet d’étendre l’environnement courant en cours par les
liaisons introduites par la clause let. Soit Env_courant cet environnement, la définition let
n1 = e1 and … and nk = ek le transforme en :
Env_courant Env_def(let n1 = e1 and … and nk = ek, Env_courant).
où Env_def calcule l’environnement ajouté par l’ensemble des liaisons introduites par une
clause let selon la règle suivante :
ENVIRONNEMENT AJOUTE PAR UNE DEFINITION NON RECURSIVE. Si Env est un environnement
et pour tout i de 1 à k, ni est un nom de valeur et ei est une expression telle que type(ei, Env) =
ti, alors :
Env_def(let n1 = e1 and … and nk = ek, Env) =
{n1 : t1 = val(e1, Env), …, nk : tk = val(ek, Env)}.
Par exemple, si l’environnement courant est :
Env_init {rayon : float = 10., pi : float = 3.14}
la définition :
let surface = 2. *. pi *. rayon;;
le transforme en :
Env_init {rayon : float = 10., pi : float = 3.14, surface : float = 62.8}
La liaison établie par la définition globale d’un nom reste valable pendant toute la session.
Pour la changer il faut redéfinir ce nom, ce qui générera une nouvelle liaison qui cachera
l’ancienne. De plus cette redéfinition n’aura aucun effet sur les définitions antérieures ayant
utilisé ce nom. Par exemple :
#let annee_courante = 1998;;
annee_courante : int = 1998
#2000 - annee_courante;;
- : int = 2
#let age = annee_courante - 1981;;
age : int = 17
#let annee_courante = 1999;;
annee_courante : int = 1999
#2000 - annee_courante;;
- : int = 1
#age;;
- : int = 17
La valeur de age a été calculée avec l’ancienne valeur de l’année courante et n’a pas été
affectée par la redéfinition de celle-ci.
Les noms définis dans une même clause let doivent être tous différents. Si ce n’est pas le cas
Caml le signale. Par exemple :
#let lon = 5 and lon = 10;;
> Toplevel input:
>let lon = 5 and lon = 10;;
> ^^^
> Variable lon is bound twice in this pattern.
Notons de plus, que les liaisons établies par une clause let ne sont visibles qu’une fois que
cette clause a été évaluée. Dans l’exemple suivant :
#let cote = 10 and périmètre = 4 * cote;;
> Toplevel input:
>let cote = 10 and périmètre = 4 * cote;;
> ^^^^
> Variable cote is unbound.
la liaison cote = 10 n’est pas visible dans la définition de périmètre. Pour qu’elle le soit il
faut réaliser deux définitions successives :
#let cote = 10;;
cote : int = 10
#let périmètre = 4 * cote;;
périmètre : int = 40
Les règles de typage et d’évaluation d’une expression à définitions locales sont les suivantes :
TYPE ET VALEUR D’UNE EXPRESSION A DEFINITIONS LOCALES. Si Env est un environnement et
pour tout i de 1 à k, ni est un nom de valeur, ei est une expression valide dans Env, et e est une
expression telle que type(e, Env Env_def(let n1 = e1 and … and nk = ek, Env)) = t, alors :
type(let n1 = e1 and … and nk = ek in e, Env) = t,
val(let n1 = e1 and … and nk = ek in e, Env) =
val(e, Env Env_def(let n1 = e1 and … and nk = ek, Env))}.
On remarque que l’environnement est étendu par les définitions locales pour la durée de
l’évaluation de l’expression e.
Concernant la duplication des noms et la visibilité des liaisons établies, les règles sont les
mêmes que pour les définitions globales (§1.5.1). Notamment pour définir des noms locaux
qui dépendent les uns des autres, il faut d’imbriquer les expressions à définitions locales. Par
exemple :
Dans ce chapitre nous pénétrons au cœur de Caml, puisque dans ce langage un programme
n’est pas autre chose qu’une suite de définitions de valeurs ou de fonctions, suivie d’une
expression à évaluer par applications successives d’une fonction à son argument.
En Caml, les fonctions sont des valeurs à part entière : elles peuvent être argument et valeur
d’une fonction. Nous étudierons tout d’abord les fonctions à un seul argument : comment les
construire et comment les appliquer. Nous montrerons ensuite que les fonctions à plusieurs
arguments peuvent être traitées comme une suite de fonctions à un argument. Nous
terminerons par l’étude des fonctions récursives.
Une fermeture n’étant pas une valeur affichable, elle est symbolisée par <fun>.
Nous appellerons expression fonctionnelle, une expression dont la valeur est une fonction.
Une fonction est destinée à être appliquée. Soit f une expression fonctionnelle de type t1 -> t2
et e une expression de type t1. L’application de f à e est une expression de type t2, ayant la
forme suivante :
f e
(il suffit tout simplement de juxtaposer la fonction et son argument). La valeur de
l’expression e est l’argument effectif de la fonction f. Par exemple :
La fonction construite ci-dessus est anonyme. Comme toute valeur, une fonction peut être
nommée par une définition globale ou locale. Par exemple :
#let carre = function x -> x * x;;
carre : int -> int = <fun>
#let carre = (function x -> x * x) in carre (carre 5);;
- : int = 625
(où *. est la multiplication des flottants) r est liée et pi est libre. Lors de l’application d’une
fonction, l’argument formel est lié à la valeur de son argument effectif. Pour les arguments
libres deux choix sont possibles. On peut les lier à la valeur qu’ils avaient dans
l’environnement de construction de la fonction ou bien, à la valeur qu’ils ont dans son
environnement d’application. On parle de langage à portée statique dans le premier cas et de
langage à portée dynamique dans le second.
Caml est un langage à portée statique. Les langages à portée statique assurent la transparence
référentielle : quelque soit le contexte où elle apparaît une fonction fournit la même valeur
lorsque est appliquée au même argument. Montrons le sur l’exemple suivant :
#let pi = 3.14;;
pi : float = 3.14
#let surface_cercle = function r -> pi *. r *. r;;
surface_cercle : float -> float = <fun>
#surface_cercle 10.;;
- : float = 314
#let pi = 3.1416;;
pi : float = 3.1416
#surface_cercle 10.;;
- : float = 314
#let divisible_par =
function x -> function y -> (y mod x) = 0;;
divisible_par : int -> int -> bool = <fun>
#divisible_par 4 5;;
- : bool = false
#divisible_par 2 4;;
- : bool = true
La fonction divisible_par 2 indique si un entier est pair. On peut lui donner le nom
est_pair :
#let est_pair = divisible_par 2;;
est_pair : int -> bool = <fun>
#est_pair 12;;
- : bool = true
#est_pair 17;;
- : bool = false
Une fonction à n arguments construite comme une suite d’applications partielles est dite
curryfiée. Ce terme a été adopté en hommage au logicien Haskell Curry.
Les opérateurs prédéfinis infixés tels que + - * / sont eux mêmes des fonctions à deux
arguments. Ils peuvent donc faire l’objet d’applications partielles. Cela nécessite de pouvoir
les écrire en position préfixe, ce qui est possible en les faisant précéder du mot-clé prefix.
En voici un exemple classique :
#prefix +;;
- : int -> int -> int = <fun>
#let successeur = (prefix +) 1;;
successeur : int -> int = <fun>
#successeur 9;;
- : int = 10
On peut indiquer à Caml, par la directive #infix n, qu’une fonction de nom n s’appliquera
en position infixe (une directive est une instruction placée dans un programme à destination
du compilateur). Par exemple :
#let et x y = x + y;;
et : int -> int -> int = <fun>
##infix "et";;
#2 et 2;;
- : int = 4
#4 et 4;;
- : int = 8
(« …huit et huit font seize… Répétez ! dit le maître … »). Pour pouvoir appliquer à nouveau
la fonction en position préfixe, il faut envoyer à Caml la directive #uninfix.
Que s’est il passé ? Une erreur a été signalée car tout nom apparaissant dans une expression
doit appartenir à l’environnement dans lequel cette expression est typée puis évaluée. Ce n’est
pas le cas du nom fac qui est en cours de définition. Il aurait fallu écrire :
#let rec fac n = if n = 1 then 1 else n * fac (n - 1);;
fac : int -> int = <fun>
Le mot-clé rec indique à Caml que la définition de fac est récursive et donc que le nom fac
sera rencontré dans sa définition. Nous appellerons définition récursive une telle définition.
Pour que les règles de typage et d’évaluation d’une définition de fonction données au §2.1
soient applicables aux fonctions récursives, il faut que l’environnement de définition soit
étendu par une liaison entre le nom de la fonction récursive, son type et sa valeur. Ces deux
derniers n’étant pas connus à ce moment là, ils seront chacun désignés par une variable.
L’environnement ajouté par une définition d’un nom de fonction récursive peut être calculée
par la règle suivante :
ENVIRONNEMENT AJOUTE PAR UNE DEFINITION RECURSIVE. Si Env un environnement, si n est
un nom de valeur et si function x -> e est une construction de fonction, alors :
Env_def(let rec n = function x -> e, Env) = {n : t = f},
où t = type(function x -> e, Env {n : t }) et f = fermeture(x -> e, Env {n = f}).
Illustrons cette règle en calculant l’environnement ajouté par la définition de la fonction fac
dans l’environnement Envd. D’après la règle ci-dessus cet environnement est égal à {fac : t =
f} où t = type(let rec fac n = if n = 1 then 1 else n * fac (n - 1), Envd
{fac : t }) et f = fermeture(x -> e, Envd {fac = f}).
Rappelons au préalable que let rec fac n = if n = 1 then 1 else n * fac (n - 1)
est une écriture simplifiée équivalente à let rec fac = function n -> if n = 1 then 1
else n * fac (n - 1).
L’application d’une fonction récursive se déroule de la même façon que celle d’une fonction
non récursive. Montrons le en évaluant fac 2 dans un environnement Env contenant la
liaison fac : int -> int = f où f = fermeture(n -> if n = 1 then 1 else n * fac (n -
1), Envd {fac = f}). On a :
val(fac 2, Env)
= val(if n = 1 then 1 else n * fac (n - 1), Envd {fac = f, n = 2})
= val(2 * fac 1, Envd {fac = f, n = 2})
= 2 * val(fac 1, Envd {fac = f, n = 2})
= 2 * val(if n = 1 then 1 …, Envd {fac = f, n = 1})
=2 * 1
=2
Une valeur structurée est formée par assemblage de valeurs qui sont soit de même type, une
liste par exemple, soit de types différents, un n-uplet par exemple.
Caml autorise la définition de fonctions polymorphes dont le type de l’argument est variable.
Ce polymorphisme est particulièrement pratique pour construire des fonctions génériques sur
des valeurs structurées : par exemple, le calcul de la longueur d’une liste qui ne nécessite pas
la connaissance du type des éléments de cette liste ou bien l’extraction du ie élément d’un n-
uplet, qui ne nécessite pas la connaissance du type de cet élément.
Une technique classique d’accès aux éléments d’une valeur structurée est le filtrage qui
consiste à faire passer cette valeur au travers d’une structure de même forme, appelée filtre,
dans laquelle chaque composante à accéder est désignée par un nom auquel elle sera liée. Par
exemple, le filtrage de la paire (1, « un ») par le filtre (c, m) produira les deux liaisons : c = 1
et m = « un ».
Nous étudierons tout d’abord les n-uplets, ce qui nous permettra d’introduire le
polymorphisme puis le filtrage et nous terminerons par l’étude des listes.
4.1 N-uplets
Rappelons que le produit cartésien de n ensembles E1, …, En, habituellement noté E1 …
En, est l’ensemble des n-uplets (e1, …, en) tels que, pour tout i de 1 à n, ei appartient à Ei. Par
exemple, le produit cartésien des ensembles {Jean} et {Pierre, Paul, Marie} est l’ensemble
{(Jean, Pierre), (Jean, Paul), (Jean, Marie)}.
En Caml le produit cartésien de n types t1, …, tn est le type t1 * … * tn (une expression de
type) dont les éléments sont les n-uplets v1, …, vn tels que pour tout i de 1 à n, vi est une
valeur de type ti. On a donc :
ext(t1 * … * tn) = {v1, …, vn | v1 ext(t1), …, vn ext(tn)}
Par exemple "Jean", 35, true est un n-uplet de type string * int * bool. On notera
que les parenthèses ne sont pas obligatoires pour encadrer un n-uplet, elles sont cependant
conseillées pour améliorer la lisibilité.
Les n-uplets à deux éléments sont appelés des paires.
La première expression a pour valeur une paire formée d’une paire d’entiers et d’un entier, la
seconde une paire formée d’un entier et d’une paire d’entiers et la troisième un triplet
d’entiers.
Ces deux fonctions étant polymorphes nous les étudierons au §4.2 consacré au
polymorphisme
4.2 Polymorphisme
En Caml il existe un type particulier appelé paramètre de type (ou variable de type) noté 'i où
i est un identificateur (en général une lettre minuscule a, b, …). Si une valeur v est de type
« paramètre de type » alors quelque soit le type t, v peut être considérée comme étant de type
t.
Un type polymorphe (ou type paramétré) est un type dont l’expression contient au moins un
paramètre de type. Par exemple 'a et (int * 'a) sont des types polymorphes. Le premier a
pour extension l’ensemble des valeurs Caml et le deuxième, l’ensemble des paires d’un entier
et d’une valeur de type quelconque.
Une fonction polymorphe est une fonction dont le type de départ est polymorphe. Un exemple
classique est la fonction id qui appliquée à une valeur retourne une valeur égale :
#let id = function x -> x;;
id : 'a -> 'a = <fun>
#id 1;;
- : int = 1
#id "un";;
- : string = "un"
On notera que les arguments doivent être du même type, comme le prouve à contrario le
programme suivant :
#1 = true;;
> Toplevel input:
>1 = true;;
> ^^^^
> Expression of type bool
> cannot be used with type int
Les fonctions d’accès aux composantes d’une valeur structurée sont polymorphes, de façon à
les rendre indépendantes des types de ces composantes. C’est le cas notamment des fonctions
d’accès au premier et au deuxième élément d’une paire que nous avons présentées au §3.1.3 :
#fst;;
- : 'a * 'b -> 'a = <fun>
#snd;;
- : 'a * 'b -> 'b = <fun>
4.3 Filtrage
Le filtrage est une technique très utilisée dans les langages de programmation fonctionnels ou
logiques, car elle améliore énormément la lisibilité des programmes.
Un filtre visualise la structure d’une valeur et nomme certaines de ses composantes. Le
filtrage est l’opération consistant à faire passer une valeur au travers d’un filtre. Si elle réussit,
elle produit un environnement qui lie les noms (on dit aussi les variables) du filtre aux
composantes correspondantes de la valeur.
Commençons par un exemple :
let (nom, _, age, ((ville, "France") as adresse)) =
("Dupont", "Jean", 12, ("Marseille", "France"));;
ville : string = "Marseille"
adresse : string * string = "Marseille", "France"
age : int = 12
nom : string = "Dupont"
– le filtrage de "Dupont" par le filtre nom qui produit la liaison nom : string = "Dupont",
– le filtrage de "Jean" par le filtre _ qui réussit car il filtre n’importe quelle valeur,
– le filtrage de 12 par le filtre age qui produit la liaison age : int = 12,
– le filtrage de la paire ("Marseille", "France") par le filtre (ville, "France") as
adresse qui déclenche :
• le filtrage de "Marseille" par le filtre ville qui produit la liaison ville : string =
"Marseille",
• le filtrage de "France" par le filtre "France" qui réussit,
– puis produit la liaison adresse : string * string = "Marseille", "France".
Soit F un filtre et v une valeur, nous noterons env_filtré(F, v) l’environnement produit en liant
les variables de F aux composantes correspondantes de v. La fonction env_filtré définit la
sémantique du filtrage.
Tout filtre a un type qui est celui des valeurs qu’il filtre.
Nous considérerons tout d’abord un sous-ensemble des filtres de Caml :
– Le filtre _ filtre toute valeur. Si v est une valeur on a env_filtré(_, v) = {}.
– Un identificateur filtre toute valeur. Si i est un identificateur et v une valeur de type t alors
a env_filtré(i, v) = {i : t = v}. i devient le nom de la valeur v.
– Une valeur filtre toute valeur qui lui est égale. Si v est une valeur alors env_filtré(v, v) =
{}.
– Si F1, …, Fn sont des filtres alors F1, …, Fn filtre tout n-uplet v1, …, vn tel que pour tout
i de 1 à n, Fi filtre vi. On a : env_filtré(F1, …, Fn, v1, …, vn ) = env_filtré(F1, v1 ) …
env_filtré(Fn, vn ).
– Si F est un filtre et n est un identificateur, alors F as n est un filtre qui lie n à la valeur
filtrée. Si v est une valeur de type t et F est un filtre de même type, alors on a : env_filtré(F
as n, v) = env_filtré(F, v ) {n : t = v}.
– Si F est un filtre alors (F) est un filtre équivalent à F.
Nous verrons par la suite, qu’à chaque type structuré est associé un filtre dont la syntaxe est
identique à celle du constructeur des valeurs de ce type.
On notera que ces nouvelles clauses let généralisent celles étudiées au §1.5, puisqu’un
identificateur est un cas particulier de filtre. Les règles d’évaluation et de typage sont elles-
mêmes généralisées de la façon suivante :
ENVIRONNEMENT AJOUTE PAR UNE DEFINITION NON RECURSIVE. Si Env est un environnement
et pour tout i de 1 à n, Fi est un filtre et ei est une expression valide dans Env, alors :
Env_def(let F1 = e1 and … and Fn = en, Env) =
env_filtré(F1, val(e1, Env)) … env_filtré(Fn, val(en, Env)).
On remarquera que le mode de définition que nous avons présenté au chapitre 2 (function x
-> e) n’est qu’un cas particulier de définition par cas puisqu’un nom de variable est un filtre.
Par exemple :
#let moyenne (x, y) = (x +. y) /. 2.;;
moyenne : float * float -> float = <fun>
Les règles de typage et d’évaluation d’une définition de fonction énoncées au §2.1, sont
généralisées de la façon suivante :
TYPE D’UNE DEFINITION DE FONCTION. Soit Env un environnement et function F1 -> e1 | … |
Fn -> en une fonction à typer dans cet environnement. On désigne par f la fonction définie par
cette expression, par t et t’ les types de départ et d’arrivée de f, par t1, …, tn les types de F1,
…, Fn et par t’1, …, t’n, les types de e1, …, en. On pose les contraintes t = type(F1, Env) = … =
type(Fn, Env) et t’ = type(e1, Env) = … = type(en, Env). Une analyse du type de chaque sous-
expression qui compose les expressions e1, … en, permet d’établir un ensemble de contraintes
sur t et t’. Trois cas peuvent alors se présenter :
1) Les contraintes sont compatibles et déterminent complètement t et t’. On a alors type(f,
Env) = t -> t’.
2) Les contraintes sont compatibles mais ne déterminent pas t et/ou t’. Ils resteront
indéterminés et seront remplacés par des paramètres de type, en général ’a, ’b, …
3) Les contraintes sont incompatibles. Cela signifie que la fonction est mal définie.
val(function F1 -> e1 | … | Fn -> en, Env) = fermeture(F1 -> e1 | … | Fn -> en, Env).
La construction if…then…else que nous avons étudiée au §1.4 n’est pas une construction
de base de Caml. Elle est un cas particulier d’application d’une fonction de type de départ
bool. On a :
if e1 then e2 else e3
≡ (function true -> e2 | false -> e3) e1
≡ match e1 with true -> e2 | false -> e3
Par exemple :
#let un_deux_trois = function
| 1 -> "un"
| 2 -> "deux"
| 3 -> "trois";;
Toplevel input:
>....................function
> | 1 -> "un"
> | 2 -> "deux"
> | 3 -> "trois"..
Warning: this matching is not exhaustive.
un_deux_trois : int -> string = <fun>
On veut définir une fonction applicable à tout entier mais seuls les entiers 1, 2 et 3 ont été
considérés. Il faut donc définir la valeur de cette fonction pour les autres entiers. Par
exemple :
#let un_deux_trois = function
| 1 -> "un"
| 2 -> "deux"
| 3 -> "trois"
| _ -> "pas un, pas deux, pas trois";;
un_deux_trois : int -> string = <fun>
4.4 Listes
Rappelons qu’une liste est une suite finie, éventuellement vide, d’éléments. Il est habituel de
construire une nouvelle liste en ajoutant un élément e en tête d’une liste l : e est la tête de la
nouvelle liste et l sa queue. Par exemple :
cons(printemps, liste(été, automne, hiver)) = liste(printemps, été, automne, hiver)
tête(liste(printemps, été, automne, hiver)) = printemps
queue(liste(printemps, été, automne, hiver)) = liste(été, automne, hiver)
Inversement on parcourt et l’on traite les éléments d’une liste par une opération récursive qui
consiste à accéder et à traiter la tête de la liste puis à relancer l’opération sur la queue de la
liste jusqu’à atteindre une liste vide.
En Caml les listes sont homogènes, ce qui signifie qu’elles sont constituées de valeurs de
même type. Le type t list a pour éléments les listes de 0, 1, 2, … valeurs de type t. On a
donc :
ext(t list) = [] {[v1] | v1 ext(t)} {[v1; v2] | v1 ext(t), v2 ext(t)} …
Les éléments d’une liste sont encadrés par des crochets et séparés par un point-virgule. Le
symbole [] désigne la liste vide. Quelque soit le type t, la liste vide appartient au type t list.
La liste vide est donc une valeur polymorphe. Par exemple, les listes [] et [1; 3; 5; 7; 9]
sont de type int list.
Par exemple :
#[("un", 0 + 1); ("deux", 1 + 1); ("trois", 2 + 1)];;
- : (string * int) list = ["un", 1; "deux", 2; "trois", 3]
PRINCIPE DE RECURRENCE STRUCTURELLE SUR LES LISTES. Si une propriété P est vraie pour []
et si dès que P est vraie pour l alors P est vraie pour x::l, alors P est vraie pour toutes les
listes.
Select…from…where
Le langage SQL a popularisé l’opérateur « select…from…where », qui appliqué à un
ensemble de tables : (i) en construit le produit cartésien, (ii) le restreint aux lignes qui
vérifient une condition donnée, et enfin (iii) le projette sur certaines colonnes. Nous en
donnons ici une version simplifiée, la fonction select_from_where qui appliquée à une
fonction f, à une liste l et à un prédicat p extrait la liste formée par l’application de la fonction
f à chaque élément de l qui vérifie le prédicat p.
Pour définir la fonction select_from_where nous utiliserons deux fonctions intermédiaires
filter et map. La fonction filter élimine d’une liste les éléments qui ne vérifient pas un
prédicat donné. Par exemple :
filter [1; 2; 3; 4; 5; 6; 7; 8; 9] est_pair = [2; 4; 6; 8]
La fonction map applique une fonction à chaque élément d’une liste. Par exemple :
map [1; 2; 3] est_pair = [false; true; false]
Considérons maintenant une liste d’enfants, où chaque enfant est représenté par son prénom
et son âge :
#let les_enfants =
[("Claire", 13); ("Alain", 8); ("Paul", 12); ("Marie", 5)];;
les_enfants : (string * int) list = ["Claire", 13; "Alain", 8; "Paul",
12; "Marie", 5]
La requête « Quels sont les prénoms des enfants de plus de 10 ans » peut s’exprimer de la
façon suivante :
#let prénom = function (p, a) -> p
and plus_de_10_ans = function (p, a) -> a > 10
in
select_from_where prénom les_enfants plus_de_10_ans;;
- : string list = ["Claire"; "Paul"]
Un langage de programmation évolué doit offrir la possibilité de définir les types nécessaires
à la gestion d’une application informatique particulière. Par exemple, la gestion d’une
bibliothèque implique la manipulation Par exemple, les types livre, abonné ou emprunt pour
une application de gestion de bibliothèque.
Caml permet de construire de nouveaux types par produit ou union de types existants : les
premiers sont appelés types enregistrement et les seconds, types union. Caml permet de plus
la définition de types paramétrés ou polymorphes. Par exemple : le type « pile de valeurs de
type quelconque », qui pourra ensuite être instancié selon les besoins, en « pile d’entiers »,
« piles de caractères », etc.
Nous étudierons successivement les types enregistrements, les types unions et les types
paramétrés et nous présenterons de nombreux exemples les utilisant.
où t, C1, …, Cn sont des identificateurs et t1, … tn sont des expressions de types. Cette phrase
définit un type enregistrement de nom t, dont les instances sont les enregistrements {C1 = v1;
…; Cn = vn} tels que pour tout i de 1 à n, vi est de type ti. On a donc :
ext(t) = {{C1 = v1; …; Cn = vn} | v1 ext(t1), …, vn ext(tn)}
Les identificateurs C1, …, Cn sont les noms des champs du type t et v1, …, vn sont les valeurs
de ces champs. Les noms de champ C1, …, Cn sont spécifiques au type t et ne peuvent donc
pas être des noms de champs d’un autre type enregistrement. Dans un enregistrement l’ordre
des champs n’a pas d’importance.
Par exemple :
#type enfant = {Prénom: string; Age: int};;
Type enfant defined.
Le type enfant est un type enregistrement qui a deux champs Prénom et Age. La valeur {Age
= 12; Prénom = "Amandine"}, par exemple, est une valeur de type enfant.
Par exemple :
#{Prénom = "Jean-" ^ "Paul"; Age = 5 + 1};;
- : enfant = {Prénom = "Jean-Paul"; Age = 6}
5.1.4 Filtrage
Si t est un type défini par t = {C1: t1; … ; Cn: tn} et si, pour tout i de 1 à n, vi est une
valeur de type ti et Fi est un filtre de type ti, alors :
{C 1 = F1; …; Cn = Fn}
est un filtre de type t tel que :
env_filtré({C1 = v1; … ; Cn = vn}, {C1 = F1; … ;Cn = Fn}) =
env_filtré(v1, F1) … env_filtré(vn, Fn)
Par exemple :
#let age = function {Prénom = _; Age = a} -> a;;
age : enfant -> int = <fun>
#age jp;;
- : int = 6
Puisqu’un nom de champ est attaché à un et un seul type, il est possible, dans un filtre,
d’omettre les champs non concernés. La définition de age dans l’exemple précédent peut
alors s’exprimer par :
#let age = function {Age = a} -> a;;
age : enfant -> int = <fun>
Le type forme est un type union qui a trois constructeurs Rectangle, Carré et Cercle. Les
valeurs Rectangle (15.5, 20.0), Carré 12.3 et Cercle 10.25 sont de type forme.
Un type union peut avoir une seule composante. Par exemple :
#type pile = Pile of int list;;
Type pile defined.
ou bien avoir certaines composantes constantes (c’est à dire réduites à leur constructeur). Par
exemple :
#type couleur_primitive = Rouge | Jaune | Bleu;;
Type couleur_primitive defined.
5.2.3 Filtrage
Si t un type union défini par t = C1 t1 | … | Cn tn et si F est un filtre de type ti {t1, …, tn}
alors :
Ci F
est un filtre de type t tel que env_filtré(Ci F, Ci v) = env_filtré(F, v).
Voici quelques exemples de manipulation de valeurs de type forme ou pile définis ci-
dessus :
#let surface = function
| Rectangle (lon, lar) -> lon *. lar
| Carré côté -> côté *. côté
| Cercle rayon -> 3.14 *. rayon *. rayon;;
surface : forme -> float = <fun>
#surface (Cercle 10.);;
- : float = 314
#let empiler i (Pile p) = Pile (i::p);;
empiler : int -> pile -> pile = <fun>
#let dépiler = function
| Pile (i::p) -> i
| Pile [] -> failwith "pile_vide";;
dépiler : pile -> int = <fun>
#dépiler (empiler 3 (Pile [1; 2]));;
- : int = 3
pièce A
pièce B pièce C
pièce D
Le cas Cercle n’a pas été prévu. La fonction périmètre n’est donc applicable qu’aux
rectangles et aux carrés :
#périmètre (Carré 4.);;
- : float = 16
(l’exception Match_failure … a été déclenchée par le système qui n’a pu filtrer la valeur
Cercle 10.).
Le défaut de cette représentation est de ne pas mettre clairement en évidence le fait qu’une
pièce soit simple ou composée : une pièce simple est caractérisée par une liste de composants
vide. On peut le faire en stipulant explicitement qu’une pièce est soit simple, soit composée,
qu’une pièce simple est décrite par son nom et qu’une pièce composée est décrite par un
enregistrement à deux champs : son nom et la liste des pièces qui la compose. On représentera
donc les pièces comme des valeurs du type pièce défini par :
et les descriptions de pièces composées comme des valeurs du type pièce_composée défini
par :
type pièce_composée = {Nom: string; Composants: pièce list}
La pièce A de la figure 5.1 pourra alors être représentée par la valeur suivante de type pièce :
Composée {Nom = "A";
Composants = [Simple "B";
Composée {Nom = "C";
Composants = [Simple "D"]}]}
Le paramètre de type 'a représente le type des éléments de la liste. Il faut bien faire attention
à la signification de cette définition. Elle stipule que les éléments d’une liste doivent avoir le
même type et non que chaque élément peut avoir un type quelconque. Voici, par exemple, ce
que répondra Caml si l’on essaie de construire une liste dont les éléments n’ont pas tous le
même type :
#1::"x"::[];;
Toplevel input:
>1::"x"::[];;
> ^^^^^^^
This expression has type string list,
but is used with type int list.
Son type est le type paramétré : 'a -> 'a list -> bool qui signifie que l’élément dont on
teste l’appartenance à la liste doit avoir le même type 'a que les éléments de cette liste. Si
cette condition n’est pas vérifiée, voici ce que répondra Caml :
#appartient "x" 1::2::[];;
Toplevel input:
>appartient "x" 1::2::[];;
> ^
This expression has type int,
but is used with type string list.
Lorsque Caml rencontre "x", il lie le paramètre de type 'a au type string et en déduit que
l’on veut tester l’appartenance à une liste de chaînes de caractères. Il signale donc une erreur
lorsqu’il rencontre la liste 1::2::[] qui est de type int list.
ENVIRONNEMENT AJOUTE PAR UNE DEFINITION DE TYPE. Si Env est un environnement, alors :
Env_def(type t = {C1: t1; …; Cn: tn}, Env) =
Env {type t, cons {C1 = t1; …; Cn = tn} -> t},
Env_def(type t = C1 of t1 | … | Cn of tn, Env) =
Env {type t, cons C1 : t1 -> t , …, cons Cn : tn -> t}.
Les noms de types et les noms de constructeurs ont été préfixés respectivement par les mots-
clés type et cons afin de ne pas les confondre avec les noms de valeur, car en Caml ces trois
ensembles de noms sont disjoints (c’est-à-dire qu’une valeur, un type ou un constructeur
peuvent avoir le même nom).
5.6 Exemples
5.6.1 Calculette
A titre d’exemple, nous allons programmer une calculette permettant d’évaluer des
expressions arithmétiques construites à partir des nombres flottants et des cinq opérations
classiques d’addition, de soustraction, de multiplication, de division et de calcul de l’opposé.
Il faut tout d’abord choisir une représentation des expressions soumises à la calculette. Nous
les représenterons comme des valeurs du type récursif expr défini de la façon suivante :
#type expr =
| Nb of float
| Add of expr * expr
| Soust of expr * expr
| Mult of expr * expr
| Div of expr * expr
| Opp of expr;;
Type expr defined.
Par exemple, l’expression arithmétique (5,2 (35,0 + 19,3)) sera représentée par la valeur
de type expr suivante :
Opp (Mult (Nb 5.2, Add (Nb 35.0, Nb 19.3)))
Remarquons encore une fois la souplesse apportée par la programmation par filtrage. Il suffit
de prévoir un cas pour chaque constructeur du type expr.
Essayons :
#let mon_expression = Opp (Mult ((Nb 5.2), (Add (Nb 35.0, Nb 19.3))));;
mon_expression : expr = Opp (Mult (Nb 5.2, Add (Nb 35.0, Nb 19.3)))
Saturne 1
Mars 2 Terre 7
#eval mon_expression;;
- : float = -282.36
Un arbre binaire de recherche est un arbre binaire dont les étiquettes sont les éléments d’un
ensemble muni d’une relation d’ordre total et qui est tel que pour chaque nœud d’étiquette e,
les étiquettes des nœuds de son sous-arbre gauche sont toutes inférieures à e et celles des
nœuds de son sous-arbre droit sont toutes supérieures à e. L’arbre binaire de la figure 5.2, par
exemple, est un arbre binaire de recherche.
Pour chercher si un élément x appartient à l’ensemble E des étiquettes d’un arbre binaire de
recherche R, il suffit d’appliquer l’algorithme suivant : « si R est vide alors x n’appartient pas
à E, sinon, si x est égal à l’étiquette de r alors x appartient à E, sinon, si x est inférieur à
l’étiquette de r alors x appartient à E s’il appartient à l’ensemble des étiquettes du sous-arbre
gauche de r, sinon, si x est supérieur à l’étiquette de r alors x appartient à E s’il appartient à
l’ensemble des étiquettes du sous-arbre droit de R ».
Testons, par exemple, si « Neptune » appartient à l’ensemble Noms_planètes des étiquettes de
l’arbre binaire de recherche de la figure 5.2. On se place sur le nœud 1 racine de cet arbre :
« Neptune » est inférieur à l’étiquette « Saturne » du nœud 1. On descend donc sur le nœud 2
racine du sous-arbre gauche du nœud 1 : « Neptune » est supérieur à l’étiquette « Mars » du
nœud 2. On descend donc sur le nœud 4 racine du sous-arbre droit du nœud 2 : « Neptune »
est égal à l’étiquette du nœud 2, il appartient donc à l’ensemble Noms_planètes.
Testons maintenant si « Io » appartient à l’ensemble Noms_planètes. On se place sur le nœud
1 : « Io » est inférieur à l’étiquette « Saturne » du noeud 1. On descend donc sur le nœud 2 :
« Io » est inférieur à l’étiquette « Mars » du nœud 2. On descend donc sur le nœud 3 : « Io »
est inférieur à l’étiquette « Jupiter » du nœud 3. Mais le sous-arbre gauche du nœud 3 est
vide : « Io » n’appartient donc pas à l’ensemble Noms_planètes.
Montrons maintenant comment représenter de tels arbres en Caml et comment les manipuler
au travers de trois opérations : insertion d’un élément, test d’appartenance d’un élément à un
ensemble et tri par ordre croissant des éléments de l’ensemble.
On définit tout d’abord le type 'a arbre dont les instances sont des arbres binaires de
recherche dont les nœuds ont pour étiquette des valeurs de type 'a :
#type 'a arbre =
| Noeud of 'a arbre * 'a * 'a arbre
| Vide;;
Type arbre defined.
L’insertion d’un nœud dans un arbre binaire nécessite la connaissance de la relation d’ordre
dont est muni l’ensemble des étiquettes de cet arbre. Nous représenterons cette relation par
une fonction qui appliquée à deux étiquettes e1 et e2 retourne Egal si e1 est égal à e2, Inf, si e1
est inférieur à e2 et Sup si e1 est supérieur à e2. Les valeurs Inf, Egal et Sup sont les instances
du type position défini par :
type position = Inf | Egal | Sup;;
#Type position defined.
L’insertion d’un nœud d’étiquette e dans un arbre binaire de recherche B produit un nouvel
arbre binaire de recherche obtenu de la façon suivante :
– si B est vide, on produit un arbre binaire de recherche dont la racine a l’étiquette e et un
sous-arbre gauche et un sous-arbre droit vide ;
– sinon, soit r l’étiquette de la racine de B, G le sous-arbre gauche de la racine de B et D son
sous-arbre droit :
• si e est égal à r, alors l’arbre produit est B puisque l’ensemble de ses étiquettes contient
déjà e ;
• sinon, si e est inférieur à r alors l’arbre produit a une racine dont l’étiquette est r ; le
sous-arbre gauche de cette racine est le résultat de l’insertion du nœud d’étiquette e dans
G et son sous-arbre droit est D ;
• sinon, si e est supérieur à r alors l’arbre produit a une racine dont l’étiquette est r ; le
sous-arbre gauche de cette racine est G et son sous-arbre droit est le résultat de
l’insertion du nœud d’étiquette e dans D.
D’où la définition de la fonction insérer_noeud qui retourne l’arbre binaire de recherche
obtenu en insérant le nœud d’étiquette e dans l’arbre binaire de recherche arbre muni de la
relation d’ordre comp :
#let rec insérer_noeud comp arbre e =
match arbre with
| Vide -> Noeud (Vide, e, Vide)
| Noeud (g, r, d) ->
(match (comp e r) with
| Inf -> Noeud ((insérer_noeud comp g e), r, d)
| Egal -> arbre
| Sup -> Noeud (g, r, (insérer_noeud comp d e)));;
insérer_noeud : ('a -> 'a -> position) -> 'a arbre -> 'a -> 'a arbre =
<fun>
En examinant le type de la fonction insérer on constate qu’il impose bien que le type 'a de
l’étiquette du nœud à insérer soit le même que le type des étiquettes de l’arbre dans lequel cet
élément est inséré et le même que le type des éléments auxquels est appliquée la fonction
comp.
L’arbre de recherche planètes est construit en insérant itérativement dans un arbre binaire de
recherche initialement vide les nœuds d’étiquettes : « Saturne », « Mars », « Neptune »,
« Jupiter », « Terre », « Uranus », « Mercure », « Pluton » et « Vénus ». Ce qui donne :
#let planètes =
let rec insérer_liste_noeuds comp arbre le =
match le with
| [] -> arbre
| e::le -> insérer_liste_noeuds comp
(insérer_noeud comp arbre e)
le
in
insérer_liste_noeuds comp_chaînes
Vide
["Saturne"; "Mars"; "Neptune"; "Jupiter";
"Terre"; "Uranus"; "Mercure"; "Pluton";
"Vénus"];;
planètes : string arbre =
Noeud
(Noeud
(Noeud (Vide, "Jupiter", Vide), "Mars",
Noeud
(Noeud (Vide, "Mercure", Vide), "Neptune",
Noeud (Vide, "Pluton", Vide))),
"Saturne",
Noeud (Vide, "Terre", Noeud (Vide, "Uranus", Noeud (Vide, "Vénus",
Vide))))
On vérifiera facilement que l’on obtient l’arbre binaire de recherche planètes est celui de la
figure 5.2.
Pour rechercher si un élément e appartient à l’ensemble E des étiquettes d’un arbre binaire de
recherche B, on applique la règle suivante, qui dérive directement de la règle de construction
des arbres binaires de recherche :
– si B est vide, alors e n’appartient pas à E ;
– sinon, soit r l’étiquette de la racine de B :
• si e est égal à r alors e appartient à E ;
• sinon, si e est inférieur à r alors rechercher e dans le sous-arbre gauche de la racine de
B;
• sinon, si e est supérieur à r alors rechercher e dans le sous-arbre droit de la racine de B.
D’où la définition de la fonction appartient qui retourne true si l’élément e appartient à
l’arbre binaire de recherche arbre ou false sinon:
#let rec appartient comp arbre e =
match arbre with
| Vide -> false
| Noeud (g, r, d) ->
(match (comp e r) with
| Inf -> appartient comp g e
| Egal -> true
| Sup -> appartient comp d e);;
appartient : ('a -> 'b -> position) -> 'b arbre -> 'a -> bool = <fun>
On peut tester cette fonction en recherchant « Neptune » puis « Io » dans l’ensemble des
étiquettes de l’arbre binaire de recherche planètes :
#appartient comp_chaînes planètes "Neptune";;
- : bool = true
#appartient comp_chaînes planètes "Io";;
- : bool = false
On vérifie bien que « Neptune » appartient à cet ensemble et que « Io » n’y appartient pas.
Pour finir, nous allons définir une fonction trier, qui permet de construire la liste triée par
ordre croissant des étiquettes de l’arbre binaire de recherche arbre. Pour réaliser ce tri, il
suffit de parcourir récursivement cet arbre dans l’ordre sous-arbre gauche – racine – sous-
arbre droit. La fonction trier peut donc être définie de la façon suivante :
#let rec trier arbre =
match arbre with
| Vide -> []
| Noeud (g, r, d) ->
(trier g) @ [r] @ (trier d);;
trier : 'a arbre -> 'a list = <fun>
Cette application a bien pour valeur la séquence des neuf noms de planètes triée par ordre
alphabétique.
En programmation fonctionnelle, les fonctions sont totales, c’est à dire qu’elles sont
applicables à tout argument qui appartient à leur type de départ. Il faut donc être capable de
traiter les cas, appelés exceptions, où cet argument n’est pas acceptable. Par exemple : une
division par zéro ou bien la recherche de la tête d’une liste vide. Ceci peut être fait par des
tests préventifs placés dans le corps des fonctions, mais ce mécanisme est très lourd car il
implique un travail important pour le programmeur et il altère la lisibilité d’un programme en
masquant son fonctionnement normal. C’est pourquoi les langages de programmation
modernes comportent un mécanisme spécifique pour le traitement des exceptions. Dans ce
chapitre nous étudions celui qui est offert par Caml.
Ce mécanisme de détection des exceptions est tout à fait insuffisant pour les deux raisons
suivantes :
– Le message généré par Caml ne permet pas de savoir en quel point du programme
l’exception a été déclenchée. Dans l’exemple ci-dessus on ne sait pas si la division par zéro
s’est produite lors de l’application de f ou de celle de g.
– Le programme est interrompu brutalement sans qu’il soit possible de remédier à la cause
de l’erreur pour que l’exécution puisse se poursuivre.
Top level
3
f1
2
f2
e1 1
e2
!!! x
Bloc de récupération B2
Bloc de récupération B1
où C est le nom du constructeur de l’exception et t est une expression de type. Par exemple :
#exception Pile_vide;;
Exception Pile_vide defined.
#exception Erreur of string;;
Exception Erreur defined.
#Pile_vide;;
- : exn = Pile_vide
#Erreur "Division par zéro en appliquant g";;
- : exn = Erreur "Division par zéro en appliquant g"
Les valeurs Pile_vide et Erreur "Division par zéro en appliquant g" sont des
instances du type exn.
Par ailleurs Caml possède un certain nombre d’exceptions prédéfinies : Failure,
Match_Failure, Invalid_argument, etc.
Il est important de noter que le type d’arrivée de la fonction raise est un paramètre de type.
Vérifions le :
#raise;;
- : exn -> 'a = <fun>
Ceci, afin que l’insertion d’un déclenchement d’exception dans une expression n’ait pas
d’influence sur le type de cette expression. Par exemple, si dans la fonction suivante :
#function x -> 365 / x;;
- : int -> int = <fun>
on insère un déclenchement d’exception en cas de division par zéro, on constate que le type
de la fonction n’est pas changé :
#function x ->
if x <> 0 then
365 / x
else
raise (Erreur "Division par zéro");;
- : int -> int = <fun>
Il existe des exceptions prédéfinies de la forme Failure m (où m est une chaîne de
caractères) qui sont déclenchées par l’application de la fonction prédéfinie failwith au
messsage m. Par exemple :
ajoute à n le nombre placé au sommet de la pile d’entiers p. Dans le cas où la pile est vide,
c’est zéro qui est ajouté à n. Vérifions le :
#ajouter 5 (Pile [1]);;
- : int = 6
#ajouter 5 (Pile []);;
- : int = 5
Jusqu’ici nous avons étudié un sous-ensemble de Caml que l’on peut qualifier de purement
fonctionnel. La seule opération mise en jeu, dans un programme purement fonctionnel, est
l’application d’une fonction. Il n’existe pas d’opérations permettant de modifier explicitement
l’état de la mémoire. Malgré son élégance, la programmation purement fonctionnelle atteint
ses limites quand il s’agit :
– de communiquer avec le monde extérieur. Par exemple, lire des données, imprimer des
résultats ;
– de modifier explicitement le contenu de certaines cases mémoires, ce qui peut être
nécessaire pour optimiser des programmes ou bien lorsque les changements d’état sont
inhérents à l’application traitée ;
– de spécifier l’ordre dans lequel doivent être évaluées certaines expressions.
On appelle effet de bord « une modification d’une case de la mémoire ou bien une interaction
avec le monde extérieur (impression ou lecture) »1. Un programme qui opère par une suite
d’effets de bord est appelé programme impératif. Caml est un langage qui combine
harmonieusement programmation fonctionnelle et programmation impérative.
C’est le sous-ensemble impératif de Caml qui fait l’objet de ce chapitre. Nous étudierons
successivement les entrées-sorties, le séquencement des expressions, les valeurs modifiables
et les boucles.
7.1 Entrées-sorties
La gestion des entrées-sorties en Caml est classique. Pour lire ou écrire sur un fichier on
utilise des canaux d’entrée ou de sortie qui sont des valeurs Caml de type in_channel ou
out_channel.
Un canal d’entrée est créé par la fonction prédéfinie open_in appliquée au nom du fichier à
ouvrir. Il est fermé en lui appliquant la fonction prédéfinie close_in. La lecture sur un canal
d’entrée est réalisée en lui appliquant les fonctions prédéfinies input_char qui lit le prochain
caractère et input_line qui lit la prochaine ligne ; l’exception End_of_file est déclenchée
lorsque la fin du fichier est atteinte.
Un canal de sortie est créé par la fonction prédéfinie open_out appliquée au nom du fichier à
ouvrir. Il est fermé en lui appliquant la fonction prédéfinie close_out. L’écriture sur un canal
de sortie est réalisée en lui appliquant les fonctions prédéfinies output_char qui écrit un
caractère et output_string qui écrit une chaîne de caractères. Par exemple, le programme
suivant écrit dans un fichier puis relit ce qu’il a écrit :
On notera que la valeur d’une ouverture est <abstract> et que le type d’arrivée de la
fonction input_line est string.
Classiquement, la lecture de caractères saisis au clavier est un cas particulier de lecture de
fichier. Elle se fait sur le canal prédéfini std_in (ou stdin). Il en est de même pour
l’affichage à l’écran qui se fait sur le canal prédéfini std_out (ou std_out). Cependant par
souci de simplicité Caml offre les fonctions prédéfinies suivantes :
read_line () input_line std_in
print_string s output_string std_out s
print_char c output_char std_out c
print_int i output_string std_out (string_of_int i)
print_float f output_string std_out (string_of_float f)
print_newline () output_string std_out "\n"
Voici, par exemple, deux fonctions dont nous nous servirons par la suite :
#let un_espace () = print_string " ";;
un_espace : unit -> unit = <fun>
#let a_la_ligne () = print_newline ();;
a_la_ligne : unit -> unit = <fun>
7.2 Séquencement
L’opérateur ; permet d’imposer l’ordre dans lequel sont évaluées deux d’expressions. Si e1 et
e2 sont des expressions, alors :
e1 ; e2
est une expression qui a pour effet de bord d’évaluer e1 puis d’évaluer e2 et qui a pour valeur
la valeur de e2. Il est évident qu’une telle expression n’a d’intérêt que si e1 est une expression
à effet de bord.
L’opérateur ; est associatif à droite. Pour marquer clairement le début et la fin d’une séquence
d’expressions, cette séquence peut être encadrée par les mots-clés begin et end.
Par exemple, le programme suivant calcule la surface d’un rectangle dont la longueur et la
largeur sont demandées à l’utilisateur.
#let lon =
print_string "Donnez la longueur du rectangle en mètres ? ";
int_of_string (read_line ())
and lar =
print_string "Donnez la largeur du rectangle en mètres ? ";
int_of_string (read_line ())
in
begin
print_string "Ce rectangle a une surface de ";
print_int (lon * lar);
print_string " m2";
a_la_ligne ()
end;;
Donnez la longueur du rectangle en mètres ? 15
Donnez la largeur du rectangle en mètres ? 5
Ce rectangle a une surface de 75 m2
- : unit = ()
7.3.1 Références
Pour permettre au programmeur de modifier explicitement le contenu de la mémoire, Caml
manipule des pointeurs comme en Pascal ou en C. En Caml un pointeur vers un objet de type
t est appelé référence et est une instance du type t ref. Une référence est construite en
appliquant le constructeur ref à l’objet à référencer. Dans le programme suivant, horloge
référence un triplet constitué de l’heure, des minutes et des secondes :
#let horloge = ref (23, 21, 59);;
horloge : (int * int * int) ref = ref (23, 21, 59)
On remarque que l’opérateur := a pour type d’arrivée unit puisqu’il se réduit à un effet de
bord : la modification de la mémoire. Le programme suivant fait avancer l’horloge d’une
seconde :
#let avancer_horloge () =
horloge := match !horloge with
| (h, 59, 59) -> (h + 1, 0, 0)
| (h, m, 59) -> (h, m + 1, 0)
| (h, m, s) -> (h, m, s + 1);;
avancer_horloge : unit -> unit = <fun>
#avancer_horloge ();;
- : unit = ()
#horloge;;
- : (int * int * int) ref = ref (23, 22, 0)
Comme les autres valeurs Caml, les références peuvent être filtrées. Si F est un filtre de type t
alors ref F est un filtre de type t ref. Par exemple :
#match horloge with ref (h, _, _) -> h;;
- : int = 23
7.3.2 Tableaux
Un tableau (ou vecteur) est une suite modifiable, de longueur fixe et non nulle, de valeurs du
même type que nous appellerons éléments du tableau. Un tableau de valeurs de type t est une
instance du type t vect. Il est noté en séparant chacun de ses éléments par un ; et en
encadrant cette suite par les signes [| et |]. Par exemple le tableau [|15.0; 5.5; 10.0|]
est de type float vect.
Un tableau peut être vu comme une suite de n cases numérotées de 0 à n 1 dont chacune
contient une valeur. Le contenu des cases est initialisé à la construction du tableau et peut être
modifié par la suite. Un tableau peut être construit de deux façons différentes :
1. soit en donnant la liste de ses éléments. Par exemple :
#[|1 + 9; 2 + 8; 3 + 7; 4 + 6; 5 + 5|];;
- : int vect = [|10; 10; 10; 10; 10|]
2. soit à l’aide du constructeur make_vect en donnant le nombre d’éléments et une valeur
initiale qui sera affectée à chacun de ces éléments. Par exemple :
#make_vect 4 "";;
- : string vect = [|""; ""; ""; ""|]
L’opérateur .() permet d’extraire la valeur contenue dans une case d’un tableau et l’opérateur
<- d’affecter une valeur à une case d’un tableau. L’expression t.(i) a pour valeur la valeur
contenue dans la ie case du tableau t et l’expression t.(i) <- v a pour effet de bord d’affecter
la valeur v à la ie case du tableau t. Par exemple :
#begin
saisons.(0) <- "printemps";
saisons.(1) <- "été";
saisons.(2) <- "automne";
saisons.(3) <- "hiver";
end;;
- : unit = ()
#saisons.(3);;
- : string = "hiver"
Nous verrons au §6.4 comment se servir des boucles pour parcourir les éléments d’un tableau.
7.4 Boucles
Caml offre deux sortes de boucles pour répéter l’évaluation d’expressions à effet de bord : la
boucle while et la boucle for.
Si e1 est une expression booléenne et e2 une expression quelconque, l’expression :
while e1 do e2 done
a pour effet de bord de répéter l’évaluation de l’expression e2 tant que l’expression e1 est
vraie. L’expression e2 est évaluée au début de chaque itération. Par exemple, le programme
suivant affiche la suite des chiffres de 0 à 9 :
#let i = ref(0) in
while !i < 10 do
print_int !i;
un_espace ()
done;
a_la_ligne ();;
0 1 2 3 4 5 6 7 8 9
- : unit = ()
Jusqu’ici nous avons programmé en Caml en considérant que nous disposions d’un ensemble
de fonctions et de types prédéfinis chargés à l’ouverture d’une session interactive et
constituant l’environnement initial de la session, puis que nous définissions au fur et à mesure
des besoins un certain nombre de noms de valeurs, de types ou d’exceptions. Cette façon de
faire est impraticable pour développer de véritables logiciels. Il faut pouvoir découper un
programme en plusieurs parties qui puissent être sauvegardées et bien entendu réutilisées.
Caml offre différents outils pour cela. Nous étudierons tout d’abord une technique simple qui
est l’inclusion de fichiers. Elle permet d’enregistrer un programme Caml dans un ou plusieurs
fichiers et de charger ces fichiers en mémoire au fur et à mesure des besoins. Cette technique
n’est pas suffisante pour deux raisons : (i) les programmes doivent être recompilés à chaque
chargement, (ii) ces fichiers ne peuvent pas être développés indépendamment puisqu’il faut
s’assurer que les ensembles de noms définis dans chacun soient disjoints. Pour pallier à cette
insuffisance, Caml permet de découper une application en modules compilables séparément.
Les modules offrant un degré suffisant de généralité peuvent être insérés dans une
bibliothèque partageable par plusieurs applications.
Dans ce chapitre nous appellerons :
– entité : un type, une exception, un constructeur ou une valeur.
– programme Caml : une suite de phrases qui sont soit des expressions, soit des définitions
de valeurs, de type ou d’exception, soit des directives.
Si le fichier n’appartient pas au répertoire courant il faut préfixer le nom de ce fichier par le
nom du répertoire auquel il appartient. Si l’extension .ml est omise elle est automatiquement
rajoutée. On aurait donc pu écrire include "un_deux_trois" au lieu de include
"un_deux_trois.ml".
8.2 Modules
Un module est un morceau de programme Caml qui forme un tout et qui est susceptible d’être
réutilisé. Le texte d’un module est enregistré dans un fichier appelé fichier source du module,
qui a le même nom que celui du module et l’extension .ml. Par exemple, le module
"cercle" de texte :
type cercle = {Rayon: float};;
let pi = 3.14;;
let périmètre {Rayon = r} = 2. *. pi *. r;;
let surface {Rayon = r} = pi *. r *. r;;
– Lorsque Caml rencontre un nom abrégé, il le complète par le nom du premier module
ouvert qui définit ce nom, en parcourant ces modules dans l’ordre suivant : le module en
cours d’analyse, les modules explicitement ouverts dans l’ordre de leur ouverture, les
modules de bibliothèque ouverts dans un ordre défini lors de l’installation de Caml.
Les règles d’emploi des noms abrégés dans le texte d’un module, impliquées par ce
mécanisme sont les suivantes.
– Toutes les entités du module en cours d’écriture peuvent être désignées par leur nom
abrégé. Cette règle est tout à fait naturelle. Elle a été appliquée implicitement, dans tous les
chapitres précédents, pour le module top.
– Toutes les entités d’un module ouvert peuvent être désignées par leur nom abrégé, s’il
n’existe pas d’entité de même nom dans un module explicitement ouvert préalablement. Il
y a donc intérêt à ouvrir en premier le module le plus utilisé.
– Toutes les entités d’un module de bibliothèque peuvent être désignées sous leur nom
abrégé, s’il n’existe pas d’entité de même nom dans les modules explicitement ouverts ou
bien dans les modules de bibliothèque ouverts préalablement. Une conséquence de cette
règle est la possibilité de redéfinir les primitives de la bibliothèque tout en conservant leur
désignation. La fonction prédéfinie min, par exemple, choisit le plus petit de deux entiers.
On peut la redéfinir dans le module top pour qu’elle choisisse le plus petit entier d’une
liste non vide d’entiers :
#min;;
- : int -> int -> int = <fun>
#min 2 1;;
- : int = 1
#let rec min = function
| [] -> failwith "min est inapplicable"
| [n] -> n
| n::l -> int__min n (min l);;
min : int list -> int = <fun>
#min [3; 1; 2];;
- : int = 1
– Dans les autres cas, les définitions devront être désignées par leur nom complet. C’est le
cas de int__min dans le programme précédent.
value n : t;;
où n est le nom de la valeur et t son type.
– l’implémentation, qui est formée par les définitions des noms de valeur et celles des types
et des exceptions privées.
Le fichier contenant l’implémentation d’un module de nom m doit avoir pour nom [Link] et
celui contenant son interface [Link].
Par exemple, dans le module cercle on peut vouloir cacher la valeur pi et exporter le type
cercle et les fonctions périmètre et surface. En ce cas l’interface, enregistrée dans le
fichier [Link] contiendra les phrases :
type cercle = {Rayon: float};;
value périmètre cercle -> float;;
value surface cercle -> float;;
L’interface par défaut d’un module est constituée de toutes les définitions contenues dans ce
module.
La compilation de l’interface d’un module m produit le fichier [Link], dit fichier d’interface
compilée et celle de l’implémentation produit le fichier [Link], dit fichier de code objet. Par
exemple [Link] et [Link] pour le module cercle.
Le fichier d’interface compilée contient les informations sur les types des entités déclarées
dans le fichier d’interface. Ce fichier est indispensable pour compiler les modules qui font
appel aux entités publiques de m. Quant au fichier de code objet, il contient le code compilé
des valeurs définies dans le fichier d’implémentation.
Pour faire de même avec un module compilé, il faut appliquer la fonction prédéfinie
load_object au nom de son fichier de code objet. Par exemple :
#load_object "[Link]";;
- : unit = ()
Travailler en mode interactif est équivalent à charger de façon incrémentale le module top
qui, comme nous l’avons vu, contient toutes les définitions entrées au clavier ou par une
inclusion de fichier (include).
Au démarrage d’une session interactive, Caml charge automatiquement tous les modules de la
bibliothèque standard. Il ouvre aussi un certain nombre de modules comme nous l’avons
expliqué au §7.2.1.
#load_object "[Link]";;
- : unit = ()
#load_object "[Link]";;
- : unit = ()
#load_object "[Link]";;
- : unit = ()
5. On ouvre le module "cylindre" :
##open "cylindre";;
6. On définit un cylindre de rayon 10 et de hauteur 20, puis on en calcule sa surface et son
volume :
#let mon_cylindre = {Rayon = 10.; Hauteur = 20.};;
mon_cylindre : cylindre = {Rayon = 10.0; Hauteur = 20.0}
#surface mon_cylindre;;
- : float = 1884.0
#volume mon_cylindre;;
- : float = 6280.0
Dans ce cours nous avons étudié les principales fonctionnalités du langage Caml. Cependant
pour construire des applications en vraie grandeur il faut en plus savoir maîtriser :
– Les instructions d’installation de Caml sur diverses machines : Macintosh, PC, Unix, …
– La bibliothèque standard composée d’une bibliothèque de base, d’une bibliothèque
d’utilitaires et d’une bibliothèque graphique. La bibliothèque de base contient un
ensemble de fonctions opérant sur les types de Caml. La bibliothèque d’utilitaires contient
entre autres des fonctions de tri, un générateur de nombres aléatoires, des fonctions pour
construire et manipuler des tables de hachage, etc.. La bibliothèque graphique contient un
ensemble de primitives graphiques portables.
– Des outils divers dont les analyseurs lexicaux et syntaxiques camllex et camlyacc.
On trouvera tout cela dans le Manuel de référence du langage Caml cité dans l’introduction.
Il faut aussi savoir qu’une version orientée objet de Caml appelée Objective Caml a été
développée, elle-même téléchargeable à partir du site de l’INRIA (voir Introduction).