Enregistrements et types mutuellement récursifs
Cours complet · OCaml (option informatique), chapitre 7 · prépas MPSI et MP, option informatique
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>7.1 Introduction et motivation
Le chapitre 3 a montré les types sommes : une valeur est l'une de plusieurs formes. Il manque leur complément, les types produits nommés : une valeur qui regroupe plusieurs champs à la fois — un point a une abscisse et une ordonnée, un étudiant a un nom et une note. Le n-uplet (chapitre 1) le faisait déjà, mais par position : dans (0.0, 1.0), lequel est l'abscisse ? L'enregistrement répond en nommant les champs.
Ce chapitre présente les enregistrements (immuables et mutables), puis les types mutuellement récursifs (déclarés ensemble avec and), qui permettent de modéliser des structures où deux notions se renvoient l'une à l'autre — un arbre et la forêt de ses sous-arbres, un système de fichiers et son contenu. Tout cela relève du niveau A.2 (utilisable après rappel).
7.2 Les enregistrements
Un enregistrement (record) regroupe plusieurs valeurs, chacune désignée par un nom de champ.
On déclare un type enregistrement en listant ses champs (en minuscule) avec leur type, entre accolades :
type point = { x : float; y : float }
On construit une valeur en donnant chaque champ, et on accède à un champ par la notation pointée :
let origine = { x = 0.0; y = 0.0 }
let abscisse p = p.x
# let p = { x = 3.0; y = 4.0 };;
val p : point = {x = 3.; y = 4.}
# p.x;;
- : float = 3.
Un n-uplet float * float et l'enregistrement point portent la même information, mais l'enregistrement la nomme : impossible de confondre x et y, l'ordre d'écriture des champs n'importe pas, et le code se lit tout seul (p.x plutôt que « la première composante »). On préfère l'enregistrement dès qu'une structure a plus d'un ou deux champs, ou que les champs jouent des rôles distincts.
Pour obtenir une copie d'un enregistrement en ne changeant que certains champs, OCaml offre la notation { r with champ = v }, qui crée un nouvel enregistrement (l'original est inchangé) :
let translate_x p dx = { p with x = p.x +. dx }
7.3 Champs mutables
Par défaut, un enregistrement est immuable. On peut rendre un champ modifiable en le déclarant mutable : on l'affecte alors par la notation pointée et <-.
type compteur = { mutable valeur : int }
let incremente c = c.valeur <- c.valeur + 1
# let c = { valeur = 0 };;
val c : compteur = {valeur = 0}
# incremente c; incremente c; c.valeur;;
- : int = 2
La référence du chapitre 4 n'est rien d'autre qu'un enregistrement à un seul champ mutable. La bibliothèque la définit essentiellement par
type 'a ref = { mutable contents : 'a }
où !r est r.contents et r := v est r.contents <- v. Les enregistrements mutables généralisent donc les références à plusieurs champs.
Un enregistrement mutable est un objet partagé, comme une référence : si deux noms désignent le même enregistrement (let d = c), une modification via l'un se voit par l'autre (aliasing). On garde cela en tête, et l'on n'emploie mutable qu'à bon escient — quand l'état doit réellement évoluer dans le temps.
7.4 Types mutuellement récursifs
Deux types peuvent se référer l'un à l'autre : on les déclare ensemble avec and. C'est le pendant, pour les types, du let rec … and … qui liait deux fonctions mutuellement récursives (chapitre 1).
Un arbre général (non binaire) a une étiquette et un nombre quelconque de sous-arbres — une forêt. Arbre et forêt se définissent mutuellement :
type 'a arbre = Noeud of 'a * 'a foret
and 'a foret = 'a arbre list
Un Noeud porte une étiquette et la liste de ses fils. Une feuille est un nœud à forêt vide : Noeud (x, []).
Les fonctions qui parcourent ces types sont elles aussi mutuellement récursives : une sur l'arbre, une sur la forêt.
let rec taille_arbre a =
match a with
| Noeud (_, f) -> 1 + taille_foret f
and taille_foret f =
match f with
| [] -> 0
| a :: reste -> taille_arbre a + taille_foret reste
taille_arbre compte le nœud courant plus la taille de sa forêt ; taille_foret additionne les tailles des arbres de la liste. Chacune appelle l'autre : le and les rend mutuellement visibles.
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>7.5 Exercices résolus
Niveau (application directe du cours)
Déclarer point (x et y flottants), puis écrire distance_carree p (le carré de la distance à l'origine, pour éviter la racine).
Démonstration
type point = { x : float; y : float }
let distance_carree p = p.x *. p.x +. p.y *. p.y
On accède aux champs par p.x et p.y, et l'on combine avec les opérateurs flottants. distance_carree { x = 3.0; y = 4.0 } vaut 25..
Déclarer etudiant (nom : string, note : float), puis est_recu e (note ).
Démonstration
type etudiant = { nom : string; note : float }
let est_recu e = e.note >= 10.0
Les champs nommés rendent le code limpide : e.note dit ce qu'on teste, là où une composante de n-uplet aurait demandé de se rappeler sa position.
Déclarer compteur (un champ mutable valeur : int), puis incremente et remet_a_zero.
Démonstration
type compteur = { mutable valeur : int }
let incremente c = c.valeur <- c.valeur + 1
let remet_a_zero c = c.valeur <- 0
Les deux fonctions modifient le champ en place (<-) et renvoient (). Le champ doit être déclaré mutable, sinon l'affectation serait refusée.
Niveau (raisonnement intermédiaire)
Écrire milieu p1 p2 renvoyant le point milieu (un nouvel enregistrement).
Démonstration
let milieu p1 p2 =
{ x = (p1.x +. p2.x) /. 2.0;
y = (p1.y +. p2.y) /. 2.0 }
On construit un nouveau point dont chaque champ est la moyenne. Les enregistrements d'entrée ne sont pas modifiés — ils sont immuables.
Déclarer compte (mutable solde : int), depose c m, et retire c m qui ne retire que si le solde suffit et renvoie un booléen de succès.
Démonstration
type compte = { mutable solde : int }
let depose c m = c.solde <- c.solde + m
let retire c m =
if m <= c.solde then begin
c.solde <- c.solde - m;
true
end
else false
depose renvoie () ; retire renvoie true après avoir modifié le solde, ou false sans rien changer si le retrait est impossible. Le begin … end groupe la séquence (modification puis valeur true) en une expression.
Déclarer bulletin (nom : string, notes : float list), puis moyenne b (moyenne des notes ; précondition : au moins une note).
Démonstration
type bulletin = { nom : string; notes : float list }
let rec somme l =
match l with
| [] -> 0.0
| x :: reste -> x +. somme reste
let rec longueur l =
match l with
| [] -> 0
| _ :: reste -> 1 + longueur reste
let moyenne b = somme b.notes /. float_of_int (longueur b.notes)
Un champ peut être de n'importe quel type, ici une liste. On réutilise le filtrage du chapitre 2 sur b.notes, et la conversion float_of_int pour diviser un flottant par un entier.
À l'aide de la notation { ... with ... }, écrire augmente e qui renvoie une copie de l'étudiant avec sa note majorée d'un point (plafonnée à ).
Démonstration
let augmente e =
let nouvelle = if e.note +. 1.0 > 20.0 then 20.0 else e.note +. 1.0 in
{ e with note = nouvelle }
{ e with note = ... } crée un nouvel etudiant identique à e sauf pour le champ note ; e reste inchangé. Pratique quand un type a beaucoup de champs et qu'on n'en modifie qu'un.
Niveau (approfondissement)
Avec les types mutuellement récursifs arbre / foret, écrire taille_arbre (nombre de nœuds).
Démonstration
let rec taille_arbre a =
match a with
| Noeud (_, f) -> 1 + taille_foret f
and taille_foret f =
match f with
| [] -> 0
| a :: reste -> taille_arbre a + taille_foret reste
Deux fonctions mutuellement récursives, à l'image des deux types : taille_arbre compte pour le nœud plus la taille de sa forêt ; taille_foret parcourt la liste des sous-arbres. La feuille Noeud (x, []) a pour taille 1 + 0 = 1.
Écrire hauteur_arbre : pour une feuille, et de plus que la hauteur maximale de ses sous-arbres sinon (forêt vide hauteur ).
Démonstration
let rec hauteur_arbre a =
match a with
| Noeud (_, f) -> 1 + hauteur_foret f
and hauteur_foret f =
match f with
| [] -> 0
| a :: reste ->
let h = hauteur_arbre a and hr = hauteur_foret reste in
if h > hr then h else hr
hauteur_foret renvoie la plus grande hauteur des arbres de la forêt (et pour la forêt vide). Une feuille Noeud (x, []) a donc pour hauteur 1 + 0 = 1.
Modéliser un système de fichiers et calculer la taille totale d'un élément.
Démonstration
On combine types sommes, enregistrements, listes et récursivité mutuelle :
type element =
| Fichier of fichier
| Dossier of dossier
and fichier = { nom_f : string; taille : int }
and dossier = { nom_d : string; contenu : element list }
let rec taille_totale e =
match e with
| Fichier f -> f.taille
| Dossier d -> taille_contenu d.contenu
and taille_contenu l =
match l with
| [] -> 0
| e :: reste -> taille_totale e + taille_contenu reste
Un element est soit un fichier (somme), décrit par un enregistrement à deux champs (produit), soit un dossier dont le contenu est une liste d'éléments — d'où la récursivité mutuelle entre element et ses enregistrements. La taille totale d'un fichier est sa taille ; celle d'un dossier, la somme des tailles de son contenu. Toutes les briques du cours se rejoignent ici.
- Enregistrement (type produit nommé) :
type point = { x : float; y : float }; construction{ x = ...; y = ... }, accèsp.x. Champs en minuscule ; l'ordre d'écriture n'importe pas. - Préférer l'enregistrement au n-uplet dès que les champs ont des rôles distincts (lisibilité, pas de confusion de position). Mise à jour fonctionnelle :
{ r with champ = v }(copie immuable). - Champ mutable :
mutable, affecté parr.champ <- v; objet partagé (aliasing). Larefest un enregistrement à un champmutable contents. - Types mutuellement récursifs : déclarés ensemble avec
and; se traitent par des fonctions mutuellement récursives (let rec … and …). Exemple : arbre général / forêt. - Sommes (choix) et enregistrements (regroupement) se combinent — avec listes et récursivité — pour modéliser fidèlement un domaine (ex. système de fichiers).
7.6 Exercices d'entraînement
Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.
Thème A — Enregistrements immuables.
- [11.] Déclarer
rectangle(largeur,hauteurflottantes) et écrireaireetperimetre. - [12.] Avec
point, écriresymetrique_origine p(le point opposé) sans champ mutable. - [13.] Déclarer
date(jour,mois,annee) et écrireavant d1 d2(ordre chronologique).
Thème B — Champs mutables.
- [14.] Avec
compteur, écrireajoute c n(ajoutenà la valeur). - [15.] Déclarer
pileavec un champmutable contenu : int listet écrireempileetdepile(avecfailwithsi vide). - [16.] Expliquer la différence d'effet entre
let d = cet la création d'un nouvel enregistrement, pour uncompteurc.
Thème C — Sommes, enregistrements, listes.
- [17.] Déclarer
forme(variantCercle/Rectangle) où chaque cas porte un enregistrement de dimensions, et écrireaire. - [18.] Avec
bulletin, écriremeilleur lqui, d'unebulletin listnon vide, renvoie le nom de l'étudiant de meilleure moyenne. - [19.] Déclarer un type
jsonsimplifié (nul, booléen, nombre, chaîne, liste, objet) — un bel usage de somme récursive — et écrireprofondeur.
Thème D — Types mutuellement récursifs.
- [20.] Sur
arbre/foret, écriresomme_etiquettes(arbre d'entiers). - [21.] Sur le système de fichiers, écrire
nb_fichiers e(nombre de fichiers contenus, récursivement). - [22.] Sur le système de fichiers, écrire
contient_nom e s: un fichier ou dossier nommésexiste-t-il quelque part danse?