Adloun

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.

Définition 7.1Déclaration et notations

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.
iRemarqueEnregistrement ou n-uplet ?

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.

iRemarqueMise à jour fonctionnelle

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 &lt;-.


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
iRemarqueLa référence est un enregistrement mutable

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 &lt;- v. Les enregistrements mutables généralisent donc les références à plusieurs champs.

Attention

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

Exemple 7.2Arbre général et forêt

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)

Exercice 1 : Le type point

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

Exercice 2 : Fiche étudiant

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.

Exercice 3 : Compteur mutable

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 (&lt;-) et renvoient (). Le champ doit être déclaré mutable, sinon l'affectation serait refusée.

Niveau (raisonnement intermédiaire)

Exercice 4 : Milieu de deux points

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

Exercice 5 : Compte bancaire

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.

Exercice 6 : Champ de type liste

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.

Exercice 7 : Mise à jour fonctionnelle

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

Exercice 8 : Taille d'un arbre général

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.

Exercice 9 : Hauteur d'un arbre général

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

Exercice 10 : Un système de fichiers

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.

Synthèse du chapitre (à retenir)
  • Enregistrement (type produit nommé) : type point = { x : float; y : float } ; construction { x = ...; y = ... }, accès p.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é par r.champ &lt;- v ; objet partagé (aliasing). La ref est un enregistrement à un champ mutable 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.
Thème B — Champs mutables.
Thème C — Sommes, enregistrements, listes.
Thème D — Types mutuellement récursifs.

Continuer sur Adloun : animation, QCM, fiches, exercices