Vue d'ensemble
En MP2I tu apprends deux langages complémentaires : le C, impératif, où l'on manipule la mémoire et où l'on modifie des variables case par case ; et OCaml, fonctionnel, où l'on calcule en composant des fonctions et où l'on ne modifie (presque) rien. Cette fiche est ta première rencontre avec OCaml : les valeurs, les fonctions, les types, le conditionnel et la récursivité. L'objectif n'est pas de te faire réciter une syntaxe, mais de te faire comprendre la logique de ce langage — car elle est très différente de celle du C.
let et inférence de type ; définition et application de fonctions ; types de base int, float (opérateurs pointés), bool, char, string ; l'expression conditionnelle if … then … else … ; la récursivité avec let rec ; l'immutabilité des liaisons. Point de vigilance permanent : le typage fort qui interdit de mélanger int et float.
Prérequis
- Aucun prérequis OCaml : on part de zéro.
- Savoir ce qu'est une fonction au sens mathématique () aide énormément.
- Notion de récursivité (une suite définie par récurrence, comme ).
- Un contraste avec le C est le bienvenu : garde en tête « en C je ferais… » à chaque étape.
Le fonctionnel te déroute au début, c'est normal. Le passage de la pensée « je modifie des cases mémoire » à « je calcule des valeurs » est le vrai obstacle de l'année en info. Nos mentors alumni X · Centrale · Mines l'ont franchi et te montrent les bons réflexes dès le premier TP.
Trouver un mentor →Valeurs et liaisons : le mot-clé let
En OCaml, la brique de base est la liaison : on donne un nom à une valeur avec let. On ne déclare jamais le type : le compilateur le devine tout seul (on parle d'inférence de type).
let x = 3
let message = "bonjour"
let pi = 3.14159let x = 3on lie le nom x à la valeur 3. Le compilateur infère le type int car 3 est un entier. Aucun int x à écrire, contrairement au C.let message = "bonjour"ici message a le type string (chaîne de caractères, entre guillemets doubles), toujours inféré.let pi = 3.14159la présence d'un point décimal fait inférer le type float. Le type est donc déterminé par la forme de la valeur.Une liaison let nom = expression évalue d'abord expression, obtient une valeur, puis attache définitivement le nom nom à cette valeur. Le type de nom est celui de la valeur, inféré automatiquement.
x = 3; range 3 dans une case que l'on pourra plus tard écraser. En OCaml, let x = 3 ne crée pas de case modifiable : c'est une définition. On y revient dans la section sur l'immutabilité.
Les fonctions
Une fonction se définit elle aussi avec let : on écrit le nom, puis les paramètres, puis =, puis le corps. Ici encore, aucun type n'est écrit : tout est inféré.
let carre x = x * x
let somme a b = a + blet carre x = x * xdéfinit une fonction carre à un paramètre x. Le corps x * x utilise * (multiplication entière), donc OCaml infère que x est un int et que carre a le type int -> int (« prend un int, rend un int »).let somme a b = a + bfonction à deux paramètres, écrits l'un après l'autre séparés par une espace (pas de virgule, pas de parenthèses). Type inféré : int -> int -> int.Pour appliquer une fonction, on écrit son nom suivi de ses arguments séparés par des espaces. Les parenthèses ne sont pas obligatoires autour d'un argument simple.
let r = carre 7
let s = somme 4 10let r = carre 7on applique carre à l'argument 7. On écrit carre 7, jamais carre(7) obligatoirement. Résultat : r = 49.let s = somme 4 10on applique somme à deux arguments. En C on écrirait somme(4, 10) ; en OCaml c'est somme 4 10. Résultat : s = 14.carre (3 + 1) calcule carre 4 = 16. Sans parenthèses, carre 3 + 1 se lit (carre 3) + 1 = 10 : l'application est plus prioritaire que le +.
Une fonction se définit par let nom p1 p2 … = corps. Son application s'écrit nom arg1 arg2 … : les arguments sont juxtaposés, séparés par des espaces. L'application lie chaque paramètre à l'argument correspondant, puis évalue le corps.
Les types de base
OCaml est à typage fort et statique : chaque valeur a un type fixé, et le compilateur refuse tout mélange incohérent. Voici les types que tu rencontres dès le premier jour.
int et float : deux mondes séparés
Le type int représente les entiers. Ses opérateurs sont +, -, *, / et mod. Attention : / sur des int est la division entière (le quotient), et mod donne le reste.
let q = 7 / 2
let r = 7 mod 2let q = 7 / 2/ entre deux int est la division entière : elle tronque. 7 / 2 vaut 3, pas 3.5. Exactement comme / entre int en C.let r = 7 mod 2mod donne le reste de la division entière : 7 mod 2 = 1. C'est l'équivalent du % du C.Le type float représente les nombres à virgule. Ses opérateurs sont pointés : +., -., *., /.. C'est LE piège central du débutant.
let a = 3.0 +. 2.0
let b = 10.0 /. 4.0let a = 3.0 +. 2.0pour additionner deux float, on écrit +. (le point collé au +). 3.0 +. 2.0 vaut 5.0. Écrire 3.0 + 2.0 est une erreur de type : + attend des int.let b = 10.0 /. 4.0/. est la division flottante : 10.0 /. 4.0 vaut 2.5. À comparer avec 10 / 4 qui vaut 2 (division entière).3 + 2.0 ne compile pas : OCaml n'insère aucune conversion automatique (pas de « promotion » comme en C). Pour passer d'un monde à l'autre il faut convertir explicitement avec float_of_int ou int_of_float. Retiens : + pour les int, +. pour les float, et les deux camps ne se rencontrent pas.
bool, char et string
Trois autres types de base à connaître :
bool: les deux valeurstrueetfalse. Comparaisons :=(égalité),<,>,<=,>=,<>(différent). Connecteurs :&&(et),||(ou),not.char: un caractère unique entre apostrophes, comme'A'ou'z'.string: une chaîne entre guillemets doubles, comme"info". On concatène deux chaînes avec l'opérateur^.
let salut = "bon" ^ "jour"
let test = (3 < 5) && (2 <> 2)let salut = "bon" ^ "jour"^ est la concaténation de chaînes : "bon" ^ "jour" vaut "bonjour". Type inféré : string.let test = (3 < 5) && (2 <> 2)3 < 5 vaut true ; 2 <> 2 (« 2 différent de 2 ») vaut false ; true && false vaut false. Type inféré : bool.int → + - * / mod ; float → +. -. *. /. ; string → ^. Une grande partie des erreurs de compilation du débutant vient d'un opérateur pris dans le mauvais monde.
Le conditionnel : if … then … else …
En OCaml, if condition then A else B n'est pas une instruction : c'est une expression qui renvoie une valeur. Elle vaut A si la condition est true, sinon B. On peut donc la lier à un nom ou la passer à une fonction.
let maximum = if 5 > 3 then 5 else 3
let signe x = if x >= 0 then "positif" else "negatif"let maximum = if 5 > 3 then 5 else 3la condition 5 > 3 vaut true, donc l'expression tout entière vaut la branche then, soit 5. On lie ce résultat à maximum. Le if a produit une valeur, pas une action.let signe x = if x >= 0 then "positif" else "negatif"fonction qui renvoie une string. Les deux branches doivent avoir le même type (ici string), sinon erreur de type.else est presque toujours obligatoire. Puisque le if doit produire une valeur, il faut dire quoi renvoyer dans les deux cas. Un if … then A sans else n'est autorisé que si A est de type unit (une action sans résultat utile), cas rare en début d'année. En pratique : mets toujours un else.
=. Dans let x = 3, le = est une liaison (« on définit »). Dans if x = 0 then …, le = est le test d'égalité (« est-ce que x vaut 0 ? »), qui renvoie un bool. C'est le contexte qui tranche. Attention : contrairement au C, l'égalité s'écrit avec un seul =, pas ==.
La récursivité : let rec
OCaml n'a pas de boucle for ou while au cœur de sa logique : pour répéter un calcul, on écrit une fonction qui s'appelle elle-même. Mais une fonction ordinaire définie par let ne connaît pas son propre nom. Pour qu'elle puisse se rappeler, il faut le mot-clé rec.
Une fonction récursive est une fonction dont le corps contient un appel à elle-même. En OCaml, on la définit avec let rec nom … = …. Le mot-clé rec (pour récursif) rend le nom de la fonction visible à l'intérieur de son propre corps. Sans rec, ce nom serait « inconnu » et la compilation échouerait.
L'exemple canonique est la factorielle : , avec la convention . La définition par récurrence se traduit presque mot pour mot :
let rec fact n =
if n = 0 then 1
else n * fact (n - 1)let rec fact n =on définit une fonction récursive fact d'un paramètre n. Le rec est indispensable : sans lui, le fact de la dernière ligne serait inconnu.if n = 0 then 1le cas de base, qui arrête la récursion. Ici n = 0 est un test d'égalité (renvoie un bool) ; si n vaut 0, on renvoie 1 (car ) sans se rappeler.else n * fact (n - 1)le cas récursif : on renvoie n multiplié par le résultat de fact (n - 1). Les parenthèses autour de n - 1 sont nécessaires, sinon OCaml lirait (fact n) - 1. Chaque appel diminue n de 1, donc on finit par atteindre le cas de base.Déroulons l'appel fact 4. À chaque étape, OCaml suspend le calcul en cours (il ne connaît pas encore fact 3) et lance un nouvel appel, jusqu'au cas de base ; puis il « remonte » en multipliant.
| Étape | Appel évalué | n | Ce que renvoie l'appel |
|---|---|---|---|
| 1 (descente) | fact 4 | 4 | 4 * fact 3 — en attente |
| 2 (descente) | fact 3 | 3 | 3 * fact 2 — en attente |
| 3 (descente) | fact 2 | 2 | 2 * fact 1 — en attente |
| 4 (descente) | fact 1 | 1 | 1 * fact 0 — en attente |
| 5 (cas de base) | fact 0 | 0 | 1 — la récursion s'arrête |
| 6 (remontée) | fact 1 | 1 | 1 * 1 = 1 |
| 7 (remontée) | fact 2 | 2 | 2 * 1 = 2 |
| 8 (remontée) | fact 3 | 3 | 3 * 2 = 6 |
| 9 (remontée) | fact 4 | 4 | 4 * 6 = 24 ✓ |
On lit donc fact 4 . La descente empile les multiplications en attente ; la remontée les effectue dans l'ordre inverse.
- Écris
let rec(jamaisletseul pour une fonction qui s'appelle). - Traite d'abord le cas de base avec un
if: la plus petite entrée pour laquelle la réponse est immédiate (icin = 0). - Dans le cas récursif, appelle la fonction sur une entrée strictement plus proche du cas de base (ici
n - 1) — sinon la récursion ne s'arrête jamais. - Combine le résultat de l'appel récursif avec la valeur courante (ici la multiplication par
n).
rec ou le cas de base. Sans rec, le compilateur affiche Unbound value fact. Sans cas de base (ou avec un argument qui ne se rapproche pas de 0), la fonction s'appelle indéfiniment : c'est le débordement de pile (Stack overflow). Toute récursion a besoin d'une porte de sortie.
La récursivité, c'est le socle de tout le semestre. Tri fusion, arbres, backtracking : tout repose sur le réflexe cas de base / cas récursif vu ici. Nos mentors alumni X · Centrale · Mines t'entraînent à le poser proprement pour qu'il devienne automatique avant les premiers DS.
Trouver un mentor →L'immutabilité : une liaison ne change pas
Voici la différence de mentalité la plus importante avec le C. En C, une variable est une case mémoire que l'on écrase autant qu'on veut : x = x + 1; a un sens. En OCaml, une liaison let attache un nom à une valeur, définitivement : il n'existe pas d'affectation destructive de ce genre.
let x = 3
let x = x + 1let x = 3on crée une liaison : le nom x désigne la valeur 3. Cette valeur ne changera jamais.let x = x + 1ce n'est PAS une modification de la case x. On crée une nouvelle liaison, également nommée x, qui vaut l'ancien x (3) plus 1, soit 4. L'ancienne liaison existe toujours « en dessous » ; on l'a simplement masquée. On appelle cela le shadowing.x vaut maintenant 4 ») ressemble à une modification, mais le mécanisme est tout autre : aucune valeur n'a été écrasée en mémoire, on a juste créé une seconde définition qui cache la première. Cette distinction devient cruciale avec les fonctions et les portées.
carre 5 vaut 25, hier, aujourd'hui et partout. C'est ce qui rend les programmes fonctionnels plus faciles à prouver corrects.
Exercices corrigés
Pour chacune des expressions suivantes, donne sa valeur ou indique si elle provoque une erreur de type, en justifiant : (a) 15 / 4 ; (b) 15 mod 4 ; (c) 2.5 +. 1.5 ; (d) 2 +. 3 ; (e) "ab" ^ "cd".
Voir la correction détaillée
15 / 4 : / entre deux int est la division entière. On cherche le quotient de 15 par 4 : , donc la valeur est 3 (le reste 3 est ignoré).15 mod 4 : mod donne le reste. D'après , la valeur est 3.2.5 +. 1.5 : les deux opérandes sont des float et l'opérateur pointé +. convient. Valeur : 4.0.2 +. 3 : erreur de type. L'opérateur +. attend deux float, or 2 et 3 sont des int. OCaml n'insère aucune conversion. Il aurait fallu 2 + 3 (valeur 5) ou 2.0 +. 3.0 (valeur 5.0)."ab" ^ "cd" : ^ concatène deux string. Valeur : "abcd".Écris une fonction récursive somme telle que somme n vaut . Précise le cas de base et le cas récursif, puis déroule somme 3.
Voir la correction détaillée
let rec.let rec somme n =
if n = 0 then 0
else n + somme (n - 1)let rec somme n = déclare la fonction récursive. if n = 0 then 0 est le cas de base (arrêt, on renvoie 0). else n + somme (n - 1) ajoute n au résultat de l'appel sur n - 1, plus proche du cas de base.somme 3. somme 3 = 3 + somme 2 = 3 + (2 + somme 1) = 3 + (2 + (1 + somme 0)) = 3 + (2 + (1 + 0)). En remontant : somme 0 = 0, somme 1 = 1, somme 2 = 3, somme 3 = 6. Résultat : 6.Écris une fonction récursive puissance telle que puissance x n vaut (pour x et n entiers, n >= 0), en utilisant et . Déroule puissance 2 4, et explique pourquoi la récursion doit porter sur n et non sur x.
Voir la correction détaillée
let rec puissance x n =
if n = 0 then 1
else x * puissance x (n - 1)let rec puissance x n = : deux paramètres, x (la base) et n (l'exposant). if n = 0 then 1 : cas de base, car . else x * puissance x (n - 1) : on multiplie par x le résultat de l'appel où l'exposant a diminué de 1. La base x est recopiée inchangée à chaque appel.puissance 2 4. 2 * puissance 2 3 = 2 * (2 * puissance 2 2) = 2 * (2 * (2 * puissance 2 1)) = 2 * (2 * (2 * (2 * puissance 2 0))). Le cas de base donne puissance 2 0 = 1. En remontant : , , , . Résultat : 16.n. Le cas de base porte sur l'exposant (n = 0). Pour garantir l'arrêt, chaque appel doit rapprocher un argument de ce cas de base : c'est n qui doit décroître vers 0. Faire décroître x ne rapprocherait d'aucun cas de base et changerait le calcul.Récap final — Ce qu'il faut absolument retenir
OCaml est un langage fonctionnel à typage statique inféré. On y calcule des valeurs plutôt que de modifier des cases. Vérifie que tu maîtrises chacun de ces réflexes.
- Sais-tu lier une valeur avec
let x = …sans jamais écrire son type (inférence) ? - Sais-tu définir
let f x = …et l'appliquer parf 7, arguments juxtaposés sans virgule ? - Sais-tu que
+ - * / modsont pour lesintet+. -. *. /.pour lesfloat, et qu'on ne mélange jamais les deux ? - Sais-tu que
/entreintest la division entière (7 / 2 = 3) et que^concatène les chaînes ? - Sais-tu que
if … then … else …est une expression qui renvoie une valeur, avec les deux branches de même type ? - Sais-tu distinguer le
=de liaison du=de test d'égalité selon le contexte ? - Sais-tu qu'une fonction récursive exige
let recet pose toujours un cas de base plus un cas récursif qui s'en rapproche ? - Sais-tu dérouler
fact 4(descente puis remontée) et retrouver24? - Sais-tu qu'une liaison
letest immuable et quelet x = x + 1masque l'ancienxau lieu de le modifier ?