Ce notebook est inspiré de ce post de blog du Professeur Matt Might, qui implémente un mini langage de programmation en $\lambda$-calcul, en Python. Je vais faire la même chose en OCaml.
On rappelle que les expressions du Lambda calcul, ou $\lambda$-calcul, sont les suivantes : $$ \begin{cases} x, y, z & \text{(des variables)} \\ u v & \text{(application de deux termes}\, u, v\; \text{)} \\ \lambda x. v & \text{(lambda-function prenant la variable}\; x \;\text{et le terme}\; v \;\text{)} \end{cases} $$
Le but ne va pas être de les représenter comme ça avec des types formels en Caml, mais plutôt d'utiliser les constructions de Caml, respectivement u(v) et fun x -> v pour l'application et les fonctions anonymes, et encoder des fonctionnalités de plus haut niveau dans ce langage réduit.
Avec une grammaire BNF, si <var> désigne un nom d'expression valide (on se limitera à des noms en miniscules consistitués des 26 lettres a,b,..,z) :
<exp> ::= <var>
| <exp>(<exp>)
| fun <var> -> <exp>
| (<exp>)
In [3]:
    
let identite = fun x -> x ;;
    
    Out[3]:
In [4]:
    
let vide = fun x -> x ;;
    
    Out[4]:
In [13]:
    
let si = fun cond valeur_vraie valeur_fausse -> cond valeur_vraie valeur_fausse ;;
    
    Out[13]:
C'est très simple, du moment qu'on s'assure que cond est soit vrai soit faux tels que définis par leur comportement :
si vrai e1 e2 == e1
si faux e1 e2 == e2
In [14]:
    
let vrai = fun valeur_vraie valeur_fausse -> valeur_vraie ;;
let faux = fun valeur_vraie valeur_fausse -> valeur_fausse ;;
    
    
    Out[14]:
    
    Out[14]:
La négation est facile !
In [15]:
    
let non = fun v x y -> v y x;;
    
    Out[15]:
En fait, on va forcer une évaluation paresseuse, comme ça si l'une des deux expressions ne terminent pas, l'évaluation fonctionne quand même.
In [16]:
    
let vrai_paresseux = fun valeur_vraie valeur_fausse -> valeur_vraie () ;;
let faux_paresseux = fun valeur_vraie valeur_fausse -> valeur_fausse () ;;
    
    
    Out[16]:
    
    Out[16]:
Pour rendre paresseux un terme, rien de plus simple !
In [17]:
    
let paresseux = fun f -> fun () -> f ;;
    
    Out[17]:
In [18]:
    
type 'a nombres = ('a -> 'a) -> 'a -> 'a;;  (* inutilisé *)
type entiers_church = (int -> int) -> int -> int;;
    
    Out[18]:
    Out[18]:
$0$ est trivialement $\lambda f. \lambda z. z$ :
In [34]:
    
let zero = fun (f : ('a -> 'a)) (z : 'a) -> z ;;
    
    
    Out[34]:
$1$ est $\lambda f. \lambda z. f z$ :
In [35]:
    
let un = fun (f : ('a -> 'a)) -> f ;;
    
    Out[35]:
Avec l'opérateur de composition, l'écriture des entiers suivants est facile.
In [36]:
    
let compose = fun f g x -> f (g x);;
    
    Out[36]:
In [37]:
    
let deux = fun f -> compose f f;;  (* == compose f (un f) *)
let trois = fun f -> compose f (deux f) ;;
let quatre = fun f -> compose f (trois f) ;;
(* etc *)
    
    Out[37]:
    Out[37]:
    Out[37]:
On peut généraliser ça, avec une fonction qui transforme un entier (int) de Caml en un entier de Church :
In [38]:
    
let rec entierChurch (n : int) =
    fun f z -> if n = 0 then z else f ((entierChurch (n-1)) f z)
;;
    
    Out[38]:
Par exemple :
In [39]:
    
(entierChurch 0) (fun x -> x + 1) 0;; (* 0 *)
(entierChurch 7) (fun x -> x + 1) 0;; (* 7 *)
(entierChurch 3) (fun x -> 2*x) 1;; (* 8 *)
    
    Out[39]:
    Out[39]:
    Out[39]:
Et une fonction qui fait l'inverse (note : cette fonction n'est pas un $\lambda$-terme) :
In [40]:
    
let entierNatif c : int =
    c (fun x -> x + 1) 0
;;
    
    Out[40]:
Un petit test :
In [41]:
    
entierNatif (si vrai zero un);; (* 0 *)
entierNatif (si faux zero un);; (* 1 *)
    
    Out[41]:
    Out[41]:
In [42]:
    
entierNatif (entierChurch 100);; (* 100 *)
    
    Out[42]:
In [43]:
    
(* prend un lambda f lambda z. ... est donne vrai ssi n = 0 ou faux sinon *)
let estnul = fun n -> n (fun z -> faux) (vrai);;
    
    
    Out[43]:
In [44]:
    
(* prend un lambda f lambda z. ... est donne vrai ssi n > 0 ou faux sinon *)
let estnonnul = fun n -> n (fun z -> vrai) (faux);;
    
    
    Out[44]:
On peut proposer cette autre implémentation, qui "fonctionne" pareil (au sens calcul des $\beta$-réductions) mais est plus compliquée :
In [45]:
    
let estnonnul2 = fun n -> non (estnul n);;
    
    Out[45]:
In [46]:
    
entierNatif (si (estnul zero) zero un);; (* 0 *)
entierNatif (si (estnul un)   zero un);; (* 1 *)
entierNatif (si (estnul deux) zero un);; (* 1 *)
    
    Out[46]:
    Out[46]:
    Out[46]:
In [47]:
    
entierNatif (si (estnonnul zero) zero un);; (* 0 *)
entierNatif (si (estnonnul un)   zero un);; (* 1 *)
entierNatif (si (estnonnul deux) zero un);; (* 1 *)
    
    Out[47]:
    Out[47]:
    Out[47]:
In [48]:
    
entierNatif (si (non (estnul zero)) zero un);; (* 0 *)
entierNatif (si (non (estnul un))   zero un);; (* 1 *)
entierNatif (si (non (estnul deux)) zero un);; (* 1 *)
    
    Out[48]:
    Out[48]:
    Out[48]:
In [49]:
    
let succ = fun n f z -> f ((n f) z) ;;
    
    Out[49]:
In [50]:
    
entierNatif (succ un);; (* 2 *)
    
    Out[50]:
In [51]:
    
deux;;
succ un;;
    
    Out[51]:
    Out[51]:
On remarque qu'ils ont le même typage, mais OCaml indique qu'il a moins d'informations à propos du deuxième : ce '_a signifie que le type est contraint, il sera fixé dès la première utilisation de cette fonction.
C'est assez mystérieux, mais il faut retenir le point suivant : deux était écrit manuellement, donc le système a vu le terme en entier, il le connaît et saît que deux = fun f -> fun x -> f (f x)), pas de surprise. Par contre, succ un est le résultat d'une évaluation partielle et vaut fun f z -> f ((deux f) z). Sauf que le système ne calcule pas tout et laisse l'évaluation partielle ! (heureusement !)
Si on appelle succ un à une fonction, le '_a va être contraint, et on ne pourra pas s'en reservir :
In [54]:
    
let succ_de_un = succ un;;
    
    Out[54]:
In [55]:
    
(succ_de_un) (fun x -> x + 1);;
    
    Out[55]:
In [56]:
    
(succ_de_un) (fun x -> x ^ "0");;
    
    
In [57]:
    
(succ un) (fun x -> x ^ "0");;
(* une valeur fraîchement calculée, sans contrainte *)
    
    Out[57]:
In [30]:
    
let pred = fun n ->
    if (entierNatif n) > 0 then entierChurch ((entierNatif n) - 1)
    else zero
;;
    
    Out[30]:
In [31]:
    
entierNatif (pred deux);; (* 1 *)
    
    Out[31]:
In [32]:
    
entierNatif (pred trois);; (* 2 *)
    
    Out[32]:
In [33]:
    
let somme = fun n m f z -> n(f)( m(f)(z));;
    
    Out[33]:
In [34]:
    
let cinq = somme deux trois ;;
    
    Out[34]:
In [35]:
    
entierNatif cinq;;
    
    Out[35]:
In [36]:
    
let sept = somme cinq deux ;;
    
    Out[36]:
In [37]:
    
entierNatif sept;;
    
    Out[37]:
In [38]:
    
let produit = fun n m f z -> m(n(f))(z);;
    
    Out[38]:
On peut faire encore mieux avec l'opérateur de composition :
In [39]:
    
let produit = fun n m -> compose m n;;
    
    Out[39]:
In [40]:
    
let six = produit deux trois ;;
    
    Out[40]:
In [41]:
    
entierNatif six;;
    
    Out[41]:
In [42]:
    
let huit = produit deux quatre ;;
    
    Out[42]:
In [43]:
    
entierNatif huit;;
    
    Out[43]:
In [75]:
    
let paire = fun a b -> fun f -> f(a)(b);;
    
    Out[75]:
In [76]:
    
let gauche = fun p -> p(fun a b -> a);;
let droite = fun p -> p(fun a b -> b);;
    
    
    Out[76]:
    
    Out[76]:
In [77]:
    
entierNatif (gauche (paire zero un));;
entierNatif (droite (paire zero un));;
    
    Out[77]:
    Out[77]:
Il y a une façon, longue et compliquée (source) d'y arriver, avec des paires.
In [78]:
    
let pred n suivant premier =
    let pred_suivant = paire vrai premier in
    let pred_premier = fun p ->
        si (gauche p)
        (paire faux premier)
        (paire faux (suivant (droite p)))
    in
    let paire_finale = n pred_suivant pred_premier in
    droite paire_finale
;;
    
    Out[78]:
Malheureusement, ce n'est pas bien typé.
In [79]:
    
entierNatif (pred deux);; (* 1 *)
    
    
Pour construire des listes (simplement chaînées), on a besoin d'une valeur pour la liste vide, listevide, d'un constructeur pour une liste cons, un prédicat pour la liste vide estvide, un accesseur tete et queue, et avec les contraintes suivantes (avec vrai, faux définis comme plus haut) :
estvide (listevide) == vrai
estvide (cons tt qu) == faux
tete (cons tt qu) == tt
queue (cons tt qu) == qu
On va stocker tout ça avec des fonctions qui attendront deux arguments (deux fonctions - rappel tout est fonction en $\lambda$-calcul), l'une appellée si la liste est vide, l'autre si la liste n'est pas vide.
In [58]:
    
let listevide = fun survide surpasvide -> survide;;
    
    
    Out[58]:
In [59]:
    
let cons = fun hd tl -> fun survide surpasvide -> surpasvide hd tl;;
    
    Out[59]:
    
Avec cette construction, estvide est assez simple : survide est () -> vrai et surpasvide est tt qu -> faux.
In [60]:
    
let estvide = fun liste -> liste (vrai) (fun tt qu -> faux);;
    
    
    Out[60]:
Deux tests :
In [61]:
    
entierNatif (si (estvide (listevide)) un zero);; (* estvide listevide == vrai *)
entierNatif (si (estvide (cons un listevide)) un zero);; (* estvide (cons un listevide) == faux *)
    
    Out[61]:
    Out[61]:
Et pour les deux extracteurs, c'est très facile avec cet encodage.
In [62]:
    
let tete = fun liste -> liste (vide) (fun tt qu -> tt);;
let queue = fun liste -> liste (vide) (fun tt qu -> qu);;
    
    
    Out[62]:
    
    Out[62]:
In [69]:
    
entierNatif (tete (cons un listevide));;
entierNatif (tete (queue (cons deux (cons un listevide))));;
entierNatif (tete (queue (cons trois (cons deux (cons un listevide)))));;
    
    Out[69]:
    Out[69]:
    Out[69]:
Visualisons les types que Caml trouve a des listes de tailles croissantes :
In [70]:
    
cons un (cons un listevide);;  (* 8 variables pour une liste de taille 2 *)
    
    Out[70]:
In [71]:
    
cons un (cons un (cons un (cons un listevide)));;  (* 14 variables pour une liste de taille 4 *)
    
    Out[71]:
In [72]:
    
cons un (cons un (cons un (cons un (cons un (cons un (cons un (cons un listevide)))))));;  (* 26 variables pour une liste de taille 7 *)
    
    Out[72]:
Pour ces raisons là, on se rend compte que le type donné par Caml à une liste de taille $k$ croît linéairement en taille en fonction de $k$ !
Aucun espoir donc (avec cet encodage) d'avoir un type générique pour les listes représentés en Caml.
Et donc nous ne sommes pas surpris de voir cet essai échouer :
In [68]:
    
let rec longueur liste =
    liste (zero) (fun t q -> succ (longueur q))
;;
    
    
En effet, `longueur` devrait être bien typée et `liste` et `q` devraient avoir le même type, or le type de `liste` est strictement plus grand que celui de `q`...
On peut essayer de faire une fonction ieme.
On veut que ieme zero liste = tete et ieme n liste = ieme (pred n) (queue liste).
En écrivant en haut niveau, on aimerait pouvoir faire :
In [87]:
    
let pop liste =
    si (estvide liste) (listevide) (queue liste)
;;
    
    
In [86]:
    
let ieme n liste =
    tete (n pop liste)
;;
    
    
In [55]:
    
let u = fun f -> f (f);;
    
    
A noter que même dans un langage non typé (par exemple Python), on peut définir $U$ mais son exécution échouera, soit à caude d'un dépassement de pile, soit parce qu'elle ne termine pas.
La fonction $Y$ trouve le point fixe d'une autre fonction. C'est très utile pour définir des fonctions par récurrence.
Par exemple, la factorielle est le point fixe de la fonction suivante : "$\lambda f. \lambda n. 1$ si $n \leq 0$ sinon $n * f(n-1)$" (écrite dans un langage plus haut niveau, pas en $\lambda$-calcul).
$Y$ satisfait ces contraintes : $Y(F) = f$ et $f = F(f)$. Donc $Y(F) = F(Y(F))$ et donc $Y = \lambda F. F(Y(F))$. Mais ce premier essai ne marche pas.
In [56]:
    
let rec y = fun f -> f (y(f));;
    
    Out[56]:
In [57]:
    
let fact = y(fun f n -> si (estnul n) (un) (produit n (f (pred n))));;
    
    
On utilise la $\eta$-expansion : si $e$ termine, $e$ est équivalent (ie tout calcul donne le même terme) à $\lambda x. e(x)$.
In [58]:
    
let rec y = fun f -> f (fun x -> y(f)(x));;
    
    Out[58]:
Par contre, le typage n'arrive toujours pas à trouver que l'expression suivante devrait être bien définie :
In [59]:
    
let fact = y(fun f n -> si (estnul n) (un) (produit n (f (pred n))));;