Vue d'ensemble
Jusqu'ici, une variable OCaml portait une seule valeur : un entier, un flottant, une chaîne. Mais un point du plan, c'est deux nombres ; un compte bancaire, c'est un nom et un solde. OCaml offre deux outils pour regrouper plusieurs valeurs en une seule : les n-uplets (couples, triplets…) et les enregistrements. La différence tient en un mot : dans un n-uplet on désigne une composante par sa position, dans un enregistrement par son nom.
* (ex. int * string) ; extraction par filtrage let (a, b) = c in ... et fonctions fst / snd ; déclaration d'un type enregistrement type point = { x : int; y : int } ; accès à un champ par le point p.x ; mise à jour fonctionnelle { p with x = 5 } et immutabilité de la valeur d'origine.
Prérequis
- Déclarer une valeur avec
letet une valeur locale aveclet ... in. - Connaître les types de base :
int,float,bool,string. - Savoir lire un type de fonction (fiche ocaml-premiers-pas).
- Distinguer la division entière
/(surint) des opérateurs flottants+.*./..
Bloqué dès la syntaxe OCaml ? Le fonctionnel déroute quand on vient du C. Nos mentors alumni X · Centrale · Mines t'apprennent à lire un type produit et à manipuler tuples et records sans te noyer dans les parenthèses.
Trouver un mentor →Les n-uplets (tuples)
Un n-uplet regroupe un nombre fixe de valeurs, éventuellement de types différents, dans un ordre déterminé. On l'écrit entre parenthèses, composantes séparées par des virgules.
Un n-uplet est une valeur formée de composantes ordonnées . Son type est le type produit , noté en OCaml avec l'opérateur *. Un couple a , un triplet . Le nombre et l'ordre des composantes font partie du type : on ne peut ni en ajouter ni en retirer.
let origine = (0, 0) (* couple, type int * int *)
let etudiant = ("Bob", 25, true) (* triplet, type string * int * bool *)
let a = fst origine (* première composante d'un couple *)
let b = snd origine (* seconde composante d'un couple *)let origine = (0, 0)on lie origine à un couple de deux entiers ; OCaml lui donne le type int * int (« int fois int »), car les deux composantes sont des entiers.let etudiant = ("Bob", 25, true)un triplet mélangeant trois types ; son type est string * int * bool. Les composantes peuvent être hétérogènes, contrairement à une liste où tous les éléments ont le même type.let a = fst originefst (de first) renvoie la première composante d'un couple ; ici a vaut 0. Attention : fst ne fonctionne que sur un couple, pas sur un triplet.let b = snd originesnd (de second) renvoie la seconde composante d'un couple ; ici b vaut 0.Extraire par filtrage
Pour un triplet, ou tout simplement pour nommer d'un coup toutes les composantes, on utilise le filtrage : à gauche du =, on écrit un motif qui a la même forme que le n-uplet, et OCaml relie chaque nom à la composante correspondante.
let couple = (3, 4)
let (x, y) = couple in x * x + y * y (* déballe le couple : x=3, y=4 *)let couple = (3, 4)on construit le couple (3, 4), de type int * int.let (x, y) = couple in ...le motif (x, y) a la forme d'un couple : OCaml lie x à la première composante (3) et y à la seconde (4). C'est du filtrage de motif, pas une multiplication.x * x + y * yon calcule . Le résultat de l'expression let ... in est donc 25.let fst (a, _) = a et let snd (_, b) = b. Le caractère _ est un motif « joker » qui filtre sans nommer.
(3, 4) et (4, 3) sont deux valeurs différentes. Si tu écris let (y, x) = (3, 4), alors y vaut 3 et x vaut 4 : c'est la position qui décide, pas le nom que tu donnes. Une inversion silencieuse est un bug classique.
Les enregistrements (records)
Quand un objet a plusieurs champs, désigner chaque composante par sa position devient vite illisible (qui se souvient que la 4e composante est le solde ?). L'enregistrement résout cela : chaque composante porte un nom.
Un enregistrement est une valeur formée de plusieurs champs nommés. On déclare d'abord un type nommé qui liste les champs et leurs types, puis on construit des valeurs de ce type. On accède à un champ par la notation pointée valeur.champ.
type point = { x : int; y : int } (* 1. déclaration du type *)
let p = { x = 1; y = 2 } (* 2. construction d'une valeur *)
let abscisse = p.x (* 3. accès au champ x *)type point = { x : int; y : int }on déclare un nouveau type nommé point : tout point a un champ x entier et un champ y entier. Cette ligne ne crée aucune valeur, elle décrit une forme. Les champs sont séparés par ;.let p = { x = 1; y = 2 }on construit une valeur de type point en donnant une valeur à chaque champ. L'ordre d'écriture des champs est libre : { y = 2; x = 1 } désigne exactement le même point.let abscisse = p.xp.x lit le champ x de p ; ici abscisse vaut 1. On accède par le nom du champ, jamais par une position.Mise à jour fonctionnelle
Les valeurs OCaml sont immuables : on ne modifie pas p, on fabrique une nouvelle valeur à partir de lui. La syntaxe { p with champ = ... } copie tous les champs de p et remplace seulement ceux qu'on cite.
L'expression { r with c = v } renvoie un nouvel enregistrement identique à r, sauf que le champ c y vaut v. L'enregistrement d'origine r n'est pas modifié : c'est une copie mise à jour, pas une affectation.
type point = { x : int; y : int }
let p = { x = 1; y = 2 }
let q = { p with x = 5 } (* nouveau point : x remplacé, y copié *)
let ancien = p.x (* p n'a pas changé *)let p = { x = 1; y = 2 }le point de départ : p.x = 1, p.y = 2.let q = { p with x = 5 }OCaml copie tous les champs de p, puis écrase x avec 5. Résultat : q = { x = 5; y = 2 }. Le champ y non cité est repris tel quel.let ancien = p.xp est intact : ancien vaut toujours 1. La mise à jour fonctionnelle n'a pas touché p, elle a produit une valeur distincte q.with qui évoque une modification, aucune case mémoire de p n'est réécrite. On obtient une seconde valeur ; p et q coexistent. Cette immutabilité est la règle par défaut en OCaml.
let p = { x = 1; y = 2 } sans avoir déclaré au préalable un type possédant les champs x et y provoque une erreur : OCaml ne sait pas à quel type d'enregistrement rattacher ces champs. Toujours type ... = { ... } d'abord.
n-uplet ou enregistrement : lequel choisir ?
Les deux regroupent des valeurs, mais le mode d'accès diffère radicalement.
| n-uplet | enregistrement | |
|---|---|---|
| Déclaration de type préalable | non | oui (type ... = { ... }) |
| Écriture d'une valeur | (1, 2) | { x = 1; y = 2 } |
| Accès à une composante | par position : fst, snd, filtrage | par nom : p.x |
| Lisibilité avec beaucoup de champs | faible (on compte les positions) | forte (les noms parlent) |
| Mise à jour d'une composante | reconstruire tout le n-uplet | { p with x = ... } |
- Deux ou trois valeurs, usage local et évident (un couple
(clé, valeur), un résultat de division(quotient, reste)) ? → un n-uplet, léger et sans déclaration. - Plusieurs champs, ou champs faciles à confondre (deux
intqui ne veulent pas dire la même chose) ? → un enregistrement, dont les noms empêchent les inversions. - Besoin de « modifier » un champ souvent ? → enregistrement, grâce à
{ ... with ... }.
Table de trace : milieu de deux points
On calcule le milieu de deux points à coordonnées entières. On suit la construction des valeurs puis l'accès aux champs, pas à pas.
type point = { x : int; y : int }
let milieu a b =
{ x = (a.x + b.x) / 2;
y = (a.y + b.y) / 2 }
let p1 = { x = 0; y = 0 }
let p2 = { x = 6; y = 4 }
let m = milieu p1 p2
let mx = m.xtype point = { x : int; y : int }on déclare le type point à deux champs entiers, indispensable avant toute valeur.let milieu a b = { x = (a.x + b.x) / 2; ... }fonction à deux arguments a et b (deux points) ; elle construit un nouveau point dont chaque coordonnée est la moyenne entière des coordonnées correspondantes. Le / est la division entière (troncature).let p1 = { x = 0; y = 0 }premier point, à l'origine.let p2 = { x = 6; y = 4 }second point.let m = milieu p1 p2on applique milieu : a devient p1, b devient p2.let mx = m.xon lit le champ x du milieu obtenu.| Étape | a.x | b.x | a.y | b.y | calcul du champ | valeur construite |
|---|---|---|---|---|---|---|
appel milieu p1 p2 | 0 | 6 | 0 | 4 | — | — |
champ x | 0 | 6 | (0 + 6) / 2 = 3 | { x = 3; ... } | ||
champ y | 0 | 4 | (0 + 4) / 2 = 2 | { x = 3; y = 2 } | ||
résultat m | — | { x = 3; y = 2 } ✓ | ||||
accès m.x | lecture du champ x | mx = 3 ✓ |
p1 = {x=0} et p2 = {x=5}, le champ x du milieu vaut (0 + 5) / 2 = 2, et non 2.5 : / tronque. Pour un vrai milieu, il faudrait des float et l'opérateur /..
Tuple, record, filtrage : tu confonds tout ? Un mentor alumni X · Centrale · Mines te fait manipuler ces structures sur tes propres exos jusqu'à ce que le choix devienne un réflexe.
Trouver un mentor →Exercices corrigés
On pose let c = (7, 2). Que valent fst c, snd c, et l'expression let (a, b) = c in a - b ?
Voir la correction détaillée
fst c renvoie la première composante : 7.snd c renvoie la seconde composante : 2.let (a, b) = c lie a = 7 (position 1) et b = 2 (position 2). Donc a - b = 7 - 2 = 5.Avec type point = { x : int; y : int }, écris une fonction decale_x qui prend un point p et un entier d, et renvoie un point dont l'abscisse est augmentée de d, l'ordonnée inchangée. Vérifie que le point d'origine n'est pas modifié.
Voir la correction détaillée
let decale_x p d = { p with x = p.x + d }{ p with x = p.x + d } copie y tel quel et remplace x par p.x + d.let p = { x = 1; y = 9 } puis let q = decale_x p 4 donne q = { x = 5; y = 9 }. Comme with ne mute pas, p.x vaut toujours 1 : l'original est intact.Un vecteur 3D est représenté par un triplet (float * float * float). Écris une fonction norme_carre qui renvoie le carré de la norme . Puis évalue norme_carre (1., 2., 2.).
Voir la correction détaillée
fst/snd (réservés aux couples) : on filtre.
let norme_carre (x, y, z) = x *. x +. y *. y +. z *. z(x, y, z) déballe les trois composantes. On utilise les opérateurs flottants *. et +. car les composantes sont des float.norme_carre (1., 2., 2.) : 9. (résultat flottant 9.).* au lieu de *. provoquerait une erreur de type, car * attend des int.Récap final — Ce qu'il faut absolument retenir
Deux façons de regrouper des valeurs, deux modes d'accès. Coche mentalement chaque point.
- Sais-tu écrire le type d'un couple et d'un triplet avec
*(ex.int * string,string * int * bool) ? - Sais-tu que
fstetsndne marchent que sur un couple, et qu'un triplet s'extrait par filtragelet (a, b, c) = t? - Sais-tu qu'un n-uplet s'accède par position et qu'inverser l'ordre change la valeur ?
- Sais-tu qu'un enregistrement exige une
type ... = { ... }déclarée avant toute valeur ? - Sais-tu construire
{ x = 1; y = 2 }(ordre des champs libre) et lire un champ avecp.x? - Sais-tu que
{ p with x = 5 }renvoie un nouveau record et ne modifie pasp? - Sais-tu choisir : n-uplet pour 2-3 valeurs locales, record dès qu'il y a plusieurs champs à nommer ?
- Sais-tu que
/sur lesinttronque, et qu'un milieu exact demande desfloatet/.?