Bonjour,
Dans le cadre de mon travail, je dois encoder une machine à pile. Cette dernière gère une pile que l’on peut manipuler grâce à des commandes. Pour l’exemple que je vais montrer, on va considérer que notre machine ne peut contenir que des entiers et des listes d’entiers que l’on peut manipuler grâce à 4 commandes :
- Empty : créer la pile vide
- Int(n) : empile l’entier n sur la pile
- Nil : empile la liste vide
- Cons : dépile une liste
l
, dépile un entiern
et empile une nouvelle listen::l
Ainsi le programme suivant :
1 2 3 4 5 6 | Empty Int(2) Int(5) Nil Cons Cons |
contient sur le sommet de la pile la liste [2;5]
. Par contre le programme suivant :
1 2 3 4 5 6 | Empty Int(2) Nil Cons Int(5) Cons |
n’est pas un programme valide. Passons au monde de la programmation, et pour ce cas d’étude on va prendre le langage OCaml. Ce qui m’intéresse ici c’est juste le type des instructions (Empty, Nil, …), pas comment on éxécute le programme. Une première façon de représenter les types est la suivante :
1 2 3 4 5 6 7 | type instr = | Empty | Int of int | Nil | Cons type program = instr list |
Avec cette représentation, on peut écrire notre premier programme, et ce n’est pas très compliqué de créer une fonction qui évalue ce programme. Cependant, on peut aussi écrire le second premier programme avec ce format, et cette fois l’évaluation dudit programme va planter.
On peut se poser la question cependant, si on ne pourrait pas statiquement détecter que le second programme est incorrect. En fait si on peut, et pour cela on va utiliser les GADTs. Si vous ne savez pas ce que c’est, je vous invite à aller regarder ce lien.
On va donc en utilisant les GADTs encoder ce qu’il y a sur notre pile. Pour cela on se donne deux nouveaux types :
1 2 | type empty type ('a, 'b) stack |
empty
correspond au type de la pile vide.
('a, 'b) stack
corrrespond au type de la pile qui contient à son sommet un objet de type ’a et le reste de la pile est ’b.
Ainsi la pile vide correspond à empty
, la pile qui contient à son sommet les entiers 2 et 5 : (int, (int, 'b) stack) stack
. Deux remarques sur ce type :
- la pile qui contient 2 et 5 est indistinguable de la pile qui contient 3 et 4 par exemple. Mais ici c’est attendu car on s’intéresse qu’au type des objets pour savoir si un programme est valide.
- le type est polymorphe en
'b
ce qui veut dire que l’on ne sait rien sur ce qu’il y a ensuite
Pour une raison obscure (si quelqu’un veut bien m’expliquer je suis preneur), il faut rajouter un constructeur à succ pour que la suite soit bien typée :
1 2 | type empty type ('a, 'b) stack = Wtf |
Maintenant on se donne le type des instructions :
1 2 3 4 5 | type 'a instr = | Empty : empty instr | Int : int * 'a instr -> ((int, 'a) stack) instr | Cons : ('b list, ('b, 'a) stack) stack instr -> ('b list, 'a) stack instr | Nil : 'a instr -> ('b list, 'a) stack instr |
On note que le type instr
a maintenant un paramètre polymorphe, le type de notre pile. De plus, tous les constructeurs prennent un paramètre : la pile qui a été construite précédement. En un certain sens, le type 'a instr
est une généralisation du type 'a list
. Par exemple pour Cons
ce dernier mange une pile qui contient à son sommet une liste dont les éléments sont de type 'b
et un élément de type 'b
et ressort une nouvelle pile qui contient cette fois seulement une liste de type 'b
.
Maintenant, je peux encoder mon premier programme comme :
1 | let _ = Cons(Cons(Nil(Int(5,Int(2,Empty))))) |
Par contre, si j’essaye de faire la même chose pour le second programme :
1 | let _ = Cons(Int(5,Cons(Nil(Int(2,Empty))))) |
OCaml nous sort une erreur de type. On a donc gagné !
Well…
Essayons de se faire une petite fonction : mk_list
. Cette dernière prendrait une liste d’entier OCaml et construirait un programme qui contiendrait les instructions pour créer cette pile. Le prototype de notre fonction serait le suivant :
1 | 'a instr -> int list -> (int list, 'a) succ instr |
Le premier paramètre vient du fait que notre programme est censé fonctionner sur toutes les piles. Vous remarquerez que dans les constructeurs de Nil
et Cons
je n’ai pas précisé le type des listes. En effet, on pourrait imaginer qu’ensuite notre machine manipule aussi des listes de string
par exemple. Donc on pourrait aussi vouloir créer une fonction mk_list
de type
1 | 'a instr -> ('a instr -> 'b -> ('b, 'a) succ instr) -> ('b list, 'a) succ instr |
Je vous invite à essayer 5mn de programmer ces prototypes, mais n’essayez pas trop longtemps. Je soupçonne très fortement que ce ne soit pas possible d’implémenter ces fonctions avec ces prototypes.
Si vous avez des idées pour implémenter mk_list
(quitte à modifier le prototype) je suis preneur.
Je vous souhaite une bonne journée