← Cours 1 : Lambda → OCaml Cours 2 : ADT →

Du Lambda Calcul à OCaml

ou comment tout construire à partir de rien (ou presque)

1. Introduction

Imaginez un monde où tout est fonction. Les moutons sont des fonctions, les nuages sont des fonctions, et même le camembert est une fonction. Bienvenue dans le lambda-calcul !

Inventé par Alonzo Church dans les années 1930 (oui, avant votre grand-mère), le λ-calcul est le plus petit langage de programmation universel. Il ne contient strictement que :

Et pourtant, avec ces trois briques, on peut TOUT calculer. Littéralement tout. Turing-complet, comme disent les gens sérieux.

Alonzo Church (1903‑1995) était un mathématicien américain, également connu pour la thèse de Church (ce qui est calculable correspond à ce qu'une machine de Turing peut calculer). Il a aussi inventé le λ-calcul et été le directeur de thèse d'Alan Turing. Pas mal pour un seul homme.

OCaml, de son côté, est un langage de la famille ML (Meta-Language). Son ancêtre, le MétaLangage, a été conçu dans les années 70 pour servir de langage de programmation au système de preuve LCF. Aujourd'hui, OCaml est le fruit du travail de l'Inria (Xavier Leroy, Damien Doligez, et bien d'autres). C'est un peu comme si le lambda-calcul avait eu un enfant surdoué qui a fait des études de maths et d'informatique.

Le lambda-calcul, c'est comme les Legos : avec juste des briques de base, tu peux construire un vaisseau spatial (ou au moins un langage de programmation fonctionnel). OCaml, c'est le set "Master Builder" avec des pièces déjà moulées.

2. Fonctions & Application

Tout commence ici. La brique élémentaire du lambda-calcul, c'est l'abstraction : fun x -> ... (les puristes écrivent λx. ... mais on va laisser les grecs tranquilles).

OCaml
(* La fonction la plus simple : celle qui ne fait rien *)
fun x -> x

(* Appliquée à 1 : *)
(fun x -> x) 1
- : int = 1

On appelle fun x -> x la fonction identité. Tellement simple qu'elle en est philosophique : "rends-moi ce que je t'ai donné".

Maintenant, observons la beauté du lambda-calcul : puisque les fonctions sont juste des valeurs, on peut les passer en paramètre, les retourner, les stocker… On appelle ça des citoyens de première classe (first-class citizens). En Java, les fonctions n'ont eu le droit de vote qu'en 2014 avec les lambdas. En OCaml, elles votent depuis 1996.

OCaml
(* Fonction qui prend une fonction et l'applique à 1 *)
fun f -> f 1

(* Fonction qui retourne une fonction *)
fun x -> fun y -> x

(* Composition de deux fonctions *)
fun f -> fun g -> fun x -> f (g x)
- : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>
Le saviez-vous ? La composition fun f g x -> f (g x) est tellement fondamentale qu'OCaml lui a dédié un opérateur : let (%) = fun f g x -> f (g x). En mathématiques, on note f ∘ g. On doit cette notation à… Alonzo Church, encore lui !

2.1 Fonctions anonymes

Regardez bien l'exemple précédent : on n'a aucun nom. Pas de def, pas de function nom. Juste fun. On appelle ça des fonctions anonymes (ou "lambda"). C'est le cœur du lambda-calcul : des fonctions sans nom qui se suffisent à elles-mêmes.

Un interlude philosophique : en lambda-calcul, les variables ne sont que des "trous" qu'on remplit par substitution. Quand on écrit (fun x -> x) 1, on substitue x par 1 dans le corps x, ce qui donne 1. C'est tout. C'est tellement simple que ça en devient beau.

Dans le lambda-calcul pur, il n'y a pas de let, pas de if, pas de boucle. Pas de sucre, pas d'édulcorant, pas même une petite cuillère de miel. Juste fun et des parenthèses. C'est le régime sec de la programmation.

2.2 Curryfication

Vous remarquerez que fun x -> fun y -> x est une fonction qui prend x et retourne… une fonction qui attend y et retourne x. C'est comme ça qu'on simule les fonctions à plusieurs paramètres dans le lambda-calcul : une fonction de deux arguments, c'est une fonction du premier argument qui retourne une fonction du second argument.

Cette technique s'appelle la curryfication (currying) et elle doit son nom à Haskell Curry, un autre logicien célèbre. (Oui, le langage Haskell porte aussi son nom. Ce type était partout.)

OCaml supporte un sucre pour ça :

OCaml
(* Ces deux écritures sont rigoureusement équivalentes : *)
fun x -> fun y -> x
fun x y -> x
Haskell Brooks Curry (1900‑1982) a donné son nom à la curryfication. Il a aussi découvert le combinateur Y (dont on reparlera bientôt). Une légende raconte qu'il a nommé son fils "Curry" pour pouvoir signer "H. Curry"… Bon, OK, c'est faux. Mais c'est drôle.

3. Le sucre let

Écrire (fun x -> x) 1, c'est un peu spartiate. Dans la vraie vie, on aime donner des noms aux choses. C'est là qu'intervient la construction let … in ….

Attention ! let x = 1 in x n'est PAS une affectation. C'est une définition locale. On ne "change" pas la valeur de x. On dit que x est un alias pour 1 dans l'expression qui suit in. C'est subtil, mais c'est toute la différence entre un langage fonctionnel et un langage impératif.

En fait, let x = e1 in e2 n'est que du sucre syntaxique pour (fun x -> e2) e1. Littéralement. C'est exactement la même chose.

OCaml
(* Ces trois paires sont rigoureusement équivalentes : *)

(* 1. La version lambda pure *)
(fun x -> x) 1

(* 2. Avec let *)
let x = 1 in x

(* 3. Avec let pour une fonction *)
let f = fun x -> x in f 1

(* 4. Double sucre : on "curryfie" le let *)
let f x = x in f 1

Les cas 3 et 4 montrent une évolution intéressante :

Si on déroule complètement le sucre :

OCaml
(* let f x = x in f 1 *)
(* = let f = fun x -> x in f 1 *)
(* = (fun f -> f 1) (fun x -> x) *)
(* = (fun x -> x) 1 *)
(* = 1 *)

C'est beau, non ? Tout n'est que fun déguisé.

Le let en OCaml, c'est un peu comme offrir une étiquette à une fonction anonyme. "Bonjour, je m'appelle f et je suis fun x -> x". Les fonctions anonymes, c'est bien, mais c'est plus sympa quand on connaît leur prénom.

4. Booléens de Church & if

Bon, on a des fonctions. On peut les appliquer. Mais comment on fait pour prendre des décisions ? Dans le lambda-calcul pur, il n'y a pas de true, pas de false, pas de if. Mais Church (encore lui !) a eu une idée géniale : coder les booléens avec des fonctions.

On va définir nos propres booléens (true_c, false_c) pour les distinguer des vrais booléens OCaml (true, false). C'est pour la pédagogie !

L'idée est simple :

Et if devient alors une fonction qui applique le booléen à deux branches :

OCaml
(* L'encodage de Church ! *)
let true_c  = fun t f -> t
let false_c = fun t f -> f

(* Le if de Church *)
let if_c b t f = b t f

(* Pour le plaisir, convertissons en vrais booléens : *)
let to_bool b = b true false
val true_c : 'a -> 'b -> 'a = <fun>
val false_c : 'a -> 'b -> 'b = <fun>
val if_c : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c = <fun>
val to_bool : (bool -> bool -> 'a) -> 'a = <fun>

Testons :

OCaml
if_c true_c "gagné" "perdu"
(* = true_c "gagné" "perdu" = "gagné" *)

to_bool true_c
(* = true_c true false = true *)
Vous venez de comprendre le secret ultime de l'univers : true est une fonction qui retourne son premier argument. La prochaine fois que quelqu'un vous dit "c'est vrai", demandez-lui "quel est ton premier argument ?". Vous ferez un malheur dans les dîners mondains.
Note culturelle : l'encodage de Church est une démonstration éclatante que le lambda-calcul est Turing-complet. Avec juste des fonctions, on peut coder n'importe quelle structure de données. Church a aussi proposé un encodage pour les entiers (les nombres de Church), mais on va les ignorer ici car on a des entiers natifs en OCaml. Si le cœur vous en dit, regardez ce que donne 3 en Church : fun f x -> f (f (f x)) — le nombre d'applications de f est le nombre.

4.1 Opérateurs logiques

Avec nos booléens de Church, on peut aussi définir les opérateurs logiques :

OCaml
* ET logique : b1 && b2 *)
let and_c b1 b2 = b1 b2 false_c
(* Si b1 est vrai, on retourne b2 ; sinon, faux *)

(* OU logique *)
let or_c  b1 b2 = b1 true_c b2
(* Si b1 est vrai, on retourne vrai ; sinon, b2 *)

(* NON logique *)
let not_c b     = b false_c true_c
(* On inverse les branches ! *)
val and_c : (bool -> bool -> bool) -> bool -> bool = <fun>
val or_c : (bool -> bool -> bool) -> bool -> bool = <fun>
val not_c : (bool -> bool -> bool) -> bool -> bool = <fun>

Magnifique, non ? Et si on vérifie :

OCaml
to_bool (and_c true_c false_c)  (* = false *)
to_bool (or_c  true_c false_c)  (* = true *)
to_bool (not_c true_c)           (* = false *)

À ce stade, on a des booléens qui marchent. Mais dans OCaml, on utilise les vrais booléens (true, false) et l'instruction if … then … else qui est intégrée au langage. Notez que même if pourrait être défini comme une fonction, mais OCaml en fait une construction syntaxique pour des raisons de typage et d'évaluation paresseuse.

L'if d'OCaml, c'est un peu le sucre syntaxique ultime. Au lieu d'écrire (fun b -> b "gagné" "perdu") true_c, on écrit if true then "gagné" else "perdu". C'est plus lisible, même si c'est moins "pur".

5. Sucre infixe

Écrire and_c true_c false_c, c'est clair. Mais si on devait faire de grosses expressions logiques, ça deviendrait vite le bazar. Heureusement, OCaml permet un sucre rigolo : les opérateurs infixes.

En OCaml, on peut définir des opérateurs avec des symboles comme +, -, *, &, |, etc. Il suffit de les mettre entre parenthèses dans la définition :

OCaml
(* On redéfinit nos opérateurs Church en version infixe *)
let (&) b1 b2 = b1 b2 false_c
let (|) b1 b2 = b1 true_c b2

(* Maintenant on peut écrire : *)
true_c & (false_c | true_c)
(* Au lieu de : and_c true_c (or_c false_c true_c) *)

OCaml a même des règles de précédence et d'associativité pour les opérateurs (les symboles commençant par * ou / sont prioritaires, etc.). Mais ça, c'est une autre histoire.

Pour les curieux ! La définition d'opérateurs infixes n'est pas qu'un sucre cosmétique. En OCaml, on peut aussi définir des opérateurs polymorphes et même des opérateurs de déstructuration avec ( := ). C'est utilisé massivement par des bibliothèques comme lwt avec >>= (le "bind" des monades) ou ppx pour la méta-programmation.
Les opérateurs infixes, c'est comme les rallonges électriques : ça rend les branchements plus propres, mais il ne faut pas en abuser sous peine de créer un nœud impossible à démêler. On appelle ça du "code spaghetti", et croyez-moi, personne n'aime les spaghettis en programmation.

6. Nombres prédéfinis

À ce stade, on pourrait coder les entiers à la Church. Mais on va faire une entorse à la pureté du lambda-calcul : on va supposer qu'on a des entiers natifs et des opérateurs arithmétiques prédéfinis.

Les nombres de Church : pour la culture (et pour frimer en société), sachez qu'un entier n en Church s'écrit fun f x -> f (f (f ... (f x)...)) avec n applications de f. Par exemple, 3 = fun f x -> f (f (f x)). C'est beau, mais c'est pas très efficace pour calculer factorial(1000).

En OCaml (et dans la plupart des langages sérieux), on a donc :

OCaml
42                  (* un entier *)
3 + 4               (* addition *)
10 - 3               (* soustraction *)
7 * 6                (* multiplication *)
42 / 6               (* division entière *)
5 mod 2              (* modulo *)

(* Comparaisons *)
3 = 4                (* false *)
3 <> 4               (* true (différent) *)
3 < 4                 (* true *)
3 <= 4                (* true *)

C'est pratique. Mais retenez que tout ça pourrait être encodé avec des fonctions. C'est juste… pas très efficace.

Notez bien la différence entre = (égalité structurelle en OCaml) et == (égalité physique). Dans le lambda-calcul, cette distinction n'existe pas. On a juste "deux termes qui se réduisent au même résultat". C'est plus simple. Et plus compliqué à la fois.
Les entiers natifs, c'est comme le pain en tranche : on pourrait le faire nous-mêmes, mais pourquoi se fatiguer quand c'est déjà tout prêt ? Surtout que nos tranches sont bien régulières, et le vrai lambda-calcul fait des miches difformes.

7. Récursion avec fix

On a des fonctions, des booléens, des entiers. On peut faire des calculs. Mais comment on fait une boucle ? En lambda-calcul pur, on n'a pas de while, pas de for, pas même de goto (Dieu merci). On n'a que des fonctions sans nom…

Mais alors, comment on écrit une fonction récursive sans nom ? C'est le paradoxe du lambda-calcul : pour s'appeler soi-même, il faut avoir un nom. Mais on n'a pas de noms (on a juste fun).

La solution, c'est le combinateur de point fixe, aussi appelé opérateur de récursion, aussi appelé "combinateur Y", aussi appelé "ce truc qui fait pleurer les étudiants en info".

⚠️ -rectypes obligatoire ! L'exemple suivant nécessite de compiler ou d'utiliser OCaml avec l'option -rectypes qui autorise les types récursifs. En OCaml sans -rectypes, le combinateur Y n'est pas typable. On peut lancer ocaml -rectypes pour tester ces exemples.

Le combinateur Y, c'est ça :

OCaml (-rectypes)
let fix f = (fun x -> f (x x)) (fun x -> f (x x))

… et si vous avez compris du premier coup, bravo, vous êtes soit Church, soit Curry, soit vous mentez.

Voici comment ça marche (promis, c'est pas si méchant) :

Regardez plutôt :

OCaml (-rectypes)
(* On définit fix une bonne fois pour toutes *)
let fix f = (fun x -> f (x x)) (fun x -> f (x x))

(* Notre "presque" factorielle : elle prend sa propre version en paramètre *)
let fact_step fact n =
  if n = 0 then 1
  else n * fact (n - 1)

(* Et fix fait le reste ! *)
let fact = fix fact_step
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
val fact_step : (int -> int) -> int -> int = <fun>
val fact : int -> int = <fun>

Testons :

OCaml (-rectypes)
fact 5
- : int = 120

Ça marche ! 🎉

7.1 Mais comment ça marche, fix ?

Détaillons la magie :

fix f calcule f (fix f). En effet :

OCaml (-rectypes)
(* fix f = f (fix f) — le point fixe ! *)

(* Déroulons pour fact 3 : *)
(* fact 3 = fix fact_step 3 *)
(*        = fact_step (fix fact_step) 3 *)
(*        = fact_step fact 3 *)
(*        = if 3 = 0 then 1 else 3 * fact (3 - 1) *)
(*        = 3 * fact 2 *)
(*        = 3 * (fix fact_step) 2 *)
(*        = 3 * (fact_step (fix fact_step) 2) *)
(*        = 3 * (if 2 = 0 then 1 else 2 * fact 1) *)
(*        = 3 * 2 * fact 1 *)
(*        = … *)
(*        = 3 * 2 * 1 * 1 = 6 *)
Le combinateur Y a été découvert par Haskell Curry (toujours lui !). Sa forme générale est λf. (λx. f (x x)) (λx. f (x x)). Il est l'archétype du combinateur de point fixe. Notez qu'il existe d'autres combinateurs de point fixe, comme le combinateur de Turing Θ (oui, Alan Turing a aussi joué dans la cour du lambda-calcul). Culture éclair : en OCaml sans -rectypes, la définition de fix ci-dessus n'est pas acceptée car x x nécessite un type récursif. C'est pour ça qu'on utilise -rectypes.
Le combinateur Y, c'est un peu comme un serpent qui se mord la queue. Sauf que le serpent, au lieu de mourir, il calcule la factorielle de 100. Le lambda-calcul, c'est magique.

8. Le mot-clé rec

Bon, fix c'est beau, c'est pur, c'est mathématique. Mais avouons-le : c'est pas super lisible. On doit écrire fact_step puis fix fact_step à chaque fois… c'est lourd.

Heureusement, OCaml nous offre du sucre syntaxique pour la récursion : let rec.

Au lieu d'écrire :

OCaml
let fact_step fact n = if n = 0 then 1 else n * fact (n-1)
let fact = fix fact_step

On écrit tout simplement :

OCaml
let rec fact n =
  if n = 0 then 1
  else n * fact (n - 1)

Et voilà ! let rec f x = ... est un sucre syntaxique qui permet à f de se référer à elle-même dans sa propre définition. C'est exactement comme si on écrivait let f = fix (fun f x -> ...).

Le passage de fix à let rec, c'est comme passer d'une Peugeot 103 (cyclomoteur pétaradant) à une BMW série 5 (berline allemande silencieuse). Les deux vous amènent à destination, mais l'une est plus confortable. Et moins ridicule.
Attention ! En OCaml, let rec est limité aux définitions récursives. Si vous écrivez let rec x = 1 in x, ça ne marchera pas (OCaml vous dira que x n'est pas une fonction). let rec sert uniquement pour définir des fonctions récursives (ou des valeurs récursives, mais c'est une autre histoire avec let rec … and …).

On peut aussi définir des fonctions mutuellement récursives (plusieurs fonctions qui s'appellent les unes les autres) avec let rec … and … :

OCaml
let rec pair n =
  if n = 0 then true
  else impair (n - 1)
and impair n =
  if n = 0 then false
  else pair (n - 1)
Le saviez-vous ? La récursion mutuelle vient du fait que, dans le lambda-calcul, on peut coder plusieurs fonctions récursives simultanées en utilisant le combinateur de point fixe… mais c'est encore plus tordu que le Y combinator. Le let rec … and … d'OCaml est un sucre bienvenu.

On peut aussi s'amuser à écrire let rec en termes de fix (cerveau qui explose) :

OCaml (-rectypes)
(* Ces deux définitions sont équivalentes : *)

(* Version sucre *)
let rec fact n = if n = 0 then 1 else n * fact (n - 1)

(* Version désucrée avec fix *)
let fact = fix (fun fact n -> if n = 0 then 1 else n * fact (n - 1))

9. Paires (Church)

On a des fonctions, des booléens, des entiers, de la récursion. Que nous manque-t-il ? Des structures de données ! Et comme on est en lambda-calcul pur (enfin, on fait semblant), on va encoder les paires… avec des fonctions, évidemment.

L'idée de Church pour les paires :

OCaml
(* L'encodage de Church pour les paires *)
let pair a b = fun f -> f a b

(* Première projection : fst *)
let fst p = p (fun a b -> a)

(* Seconde projection : snd *)
let snd p = p (fun a b -> b)

(* Testons ! *)
let p = pair 3 "hello"
let _ = fst p  (* = 3 *)
let _ = snd p  (* = "hello" *)
val pair : 'a -> 'b -> ('a -> 'b -> 'c) -> 'c = <fun>
val fst : (('a -> 'b -> 'a) -> 'c) -> 'c = <fun>
val snd : (('a -> 'b -> 'b) -> 'c) -> 'c = <fun>

Regardez comme c'est élégant : pair a b = fun f -> f a b. Une paire, c'est une fonction qui "retient" ses deux éléments et attend qu'on lui dise quoi en faire. Les projections fst et snd sont juste des "instructions" qu'on donne à la paire pour extraire telle ou telle composante.

En OCaml "vrai", on utilise les n-uplets (tuples) : (3, "hello") et fst/snd n'existent pas en OCaml standard (on utilise le pattern matching). Mais conceptuellement, c'est la même idée.

Les paires de Church, c'est un peu comme un sandwich : vous donnez deux ingrédients (a et b), et vous dites "débrouillez-vous avec ça". Quand vous voulez le premier ingrédient, vous ouvrez le sandwich. Le pain, c'est la fonction. Et fst, c'est le coup de fourchette pour récupérer le jambon.

On peut même définir des triplets, quadruplets, etc. :

OCaml
(* Pour les courageux : un triplet Church *)
let triple a b c = fun f -> f a b c
let proj1 t = t (fun a b c -> a)
let proj2 t = t (fun a b c -> b)
let proj3 t = t (fun a b c -> c)

10. Listes (Church) & match

On a des paires. Maintenant, on veut des listes (chainées). Une liste, c'est soit :

Church encode les listes comme des fonctions qui attendent deux arguments :

C'est exactement le principe du fold (ou réduction). Une liste de Church, c'est une fonction qui "sait se plier".

OCaml
(* Liste vide : retourne le cas de base "n" *)
let nil = fun n c -> n

(* Cons : construit une liste avec tête h et queue t *)
(* On appelle c h (t n c) pour "plier" la tête puis la queue *)
let cons h t = fun n c -> c h (t n c)

(* Exemple : liste [1; 2; 3] *)
let lst = cons 1 (cons 2 (cons 3 nil))

Pour visualiser ce qu'on a construit, on peut écrire une fonction to_list qui convertit une liste Church en liste OCaml :

OCaml
(* fold_right sur une liste Church *)
let fold lst n c = lst n c

(* Convertir une liste Church en liste OCaml *)
let to_list lst = lst [] (fun h t -> h :: t)

(* Testons ! *)
to_list lst  (* = [1; 2; 3] *)

(* Longueur d'une liste Church *)
let length lst = lst 0 (fun _ n -> n + 1)

(* Concaténer deux listes Church *)
let append l1 l2 = l1 l2 cons
val to_list : ((('a -> 'b list -> 'b list) -> 'b list -> 'b list) -> (('a -> 'b list -> 'b list) -> 'b list -> 'b list) -> 'b list) -> 'b list = <fun>
val length : ((int -> ('a -> int -> int) -> int) -> (int -> ('a -> int -> int) -> int) -> int) -> int = <fun>
val append : (((('a -> 'b -> 'b) -> 'c -> 'b) -> (('a -> 'b -> 'b) -> 'c -> 'b) -> 'd) -> (('a -> 'b -> 'b) -> 'c -> 'b) -> (('a -> 'b -> 'b) -> 'c -> 'b) -> 'd) -> ('a -> 'b -> 'b) -> 'c -> 'b = <fun>
Les listes de Church, c'est des listes tellement fonctionnelles qu'elles ont leur propre GPS intégré. Chaque élément sait exactement comment se rendre à la fin de la liste. C'est la magie du pliage (fold).

On peut aussi extraire la tête et la queue, mais attention :

OCaml
(* Attention : head et tail ne marchent pas sur liste vide ! *)
let head lst = lst (failwith "empty") (fun h _ -> h)
let tail lst = lst (failwith "empty") (fun _ t -> t)

10.1 Enfin match !

Bon, manipuler les listes avec lst n c, c'est puissant, mais c'est pas ce qu'on peut appeler "intuitif". Heureusement, OCaml a des types algébriques de données (Algebraic Data Types, ADT) — mais c'est un autre cours — et l'instruction match pour faire du pattern matching.

Le match est le sucre syntaxique ultime pour déstructurer des données. Au lieu d'écrire lst n c, on écrit :

OCaml
(* OCaml a des listes natives avec [] et :: *)
let ma_liste = [1; 2; 3]

(* Pattern matching avec match *)
let sum lst =
  match lst with
  | [] -> 0
  | h :: t -> h + sum t

C'est tellement plus clair ! On voit immédiatement les deux cas : liste vide ou liste non vide.

En fait, le pattern matching est tellement pratique qu'on l'utilise partout en OCaml :

OCaml
(-- Avec des tuples (n-uplets) --)
let fst p = match p with (a, _) -> a
let snd p = match p with (_, b) -> b

(* Avec des types personnalisés *)
type 'a mylist =
  | Nil
  | Cons of 'a * 'a mylist

let sum2 lst =
  match lst with
  | Nil -> 0
  | Cons (h, t) -> h + sum2 t

Vous remarquerez la correspondance parfaite avec notre encodage de Church :

Le pattern matching est l'une des fonctionnalités les plus puissantes des langages de la famille ML (Meta-Language). Il permet de déstructurer des données de façon concise et exhaustive. En OCaml, le compilateur vérifie même que vous avez traité tous les cas (exhaustivité). C'est une ceinture de sécurité pour votre code. Dans d'autres langages, cette fonctionnalité a été copiée (Rust, Haskell, Scala, etc.) — mais jamais égalée. On dit que les vrais développeurs ML sont addicts au pattern matching et ne peuvent plus coder sans.

10.2 Exemples concrets

Un petit florilège de pattern matching sur les listes :

OCaml
(* Fibonacci récursive *)
let rec fib n =
  match n with
  | 0 -> 0
  | 1 -> 1
  | _ -> fib (n - 1) + fib (n - 2)

(* Map sur une liste *)
let rec map f lst =
  match lst with
  | [] -> []
  | h :: t -> f h :: map f t

(* Filtrer une liste *)
let rec filter p lst =
  match lst with
  | [] -> []
  | h :: t -> if p h then h :: filter p t else filter p t

Tous ces exemples pourraient être réécrits en utilisant l'encodage de Church des listes. Mais franchement, pourquoi se priver du match ?

Le match en OCaml, c'est un peu comme un couteau suisse : ça coupe, ça ouvre, ça visse, ça dévisse. Avec le pattern matching, vous pouvez déstructurer à peu près n'importe quoi : listes, tuples, variants, options, résultats… Certains programmeurs OCaml passent tellement de temps à faire du match qu'ils en oublient de dormir. (OK, c'est une blague. Mais à peine.)

11. Conclusion

Vous venez de faire un voyage du lambda-calcul le plus pur (juste fun) jusqu'aux constructions élégantes d'OCaml (let, let rec, match).

Récapitulons le chemin parcouru :

Concept Lambda pur OCaml sucré
Fonction fun x -> x fun x -> x
Définition locale (fun x -> e2) e1 let x = e1 in e2
Fonction nommée (fun f -> f 1) (fun x -> x) let f x = x in f 1
Conditionnelle b t f if b then t else f
Récursion fix (fun f x -> ...) let rec f x = ...
Paires fun f -> f a b (a, b)
Listes fun n c -> c h (t n c) h :: t

Ce qu'il faut retenir :

  1. Tout n'est que fonction. Le lambda-calcul le prouve : avec juste fun et l'application, on peut coder l'intégralité de l'informatique.
  2. Le sucre syntaxique, c'est la classe. OCaml n'ajoute pas de nouveaux pouvoirs magiques — il rend juste les choses plus lisibles. let, if, match, let rec sont des habillages élégants du lambda-calcul.
  3. Church et Curry étaient des génies. Leurs encodages sont encore étudiés aujourd'hui, et leur influence se ressent dans tous les langages fonctionnels modernes (Haskell, OCaml, Scala, F#, Elixir…).
Pour aller plus loin :
  • Types de données algébriques (ADT) : le cœur d'OCaml. type 'a option = None | Some of 'a — le pattern matching devient alors un outil encore plus puissant.
  • GADT (Generalized Algebraic Data Types) : des types qui peuvent raisonner sur leur propres paramètres. C'est du niveau "docteur en informatique".
  • Monades : une abstraction qui permet de chaîner des calculs avec effet. Popularisées par Haskell, mais utilisables en OCaml.
  • Théorie des types : le système de types d'OCaml est basé sur le λ-calcul typé (Système F). C'est ce qui permet au compilateur de détecter vos erreurs avant même d'exécuter le code.

"Le lambda-calcul est à la programmation fonctionnelle ce que la roue est au transport : simple, élégant, et personne n'aurait dû mettre autant de temps à l'inventer."

Fait avec ❤️ et beaucoup de λ

© 2026 — laurent.thiry@uha.fr