On se propose dans ce TP d'utiliser une technique algorithmique appelée zipper, permettant de parcourir une structure de donnée, typiquement un arbre, en appliquant des transformations locales. Nous regarderons le cas du zipper de liste, un peu plus simple. Nous donnerons une première implémentation du zipper de liste. Ensuite, nous lui donnerons une interface de monade d'état. Nous utiliserons cette interface pour implémenter une machine de Turingcalculant des additions, ou une autre machine qui est une solution du problème de Busy Beaver.
Nous avons appris que les listes s'utilisent en manipulant uniquement une extrémité. Cela exclut les applications nécessitant de manipuler des éléments au milieu de la liste. Les zippers apportent une solution partielle à ce problème : plutôt que de coder directement la liste, un zipper code une position dans une liste. Par exemple :
Nous pouvons donc définir un zipper comme :
Une façon de comprendre le zipper de liste est d'imaginer un ruban magnétique, et une tête de lecture qui lit un endroit particulier du ruban, comme dans un vieux magnétophone. La tête magnétique peut lire ou modifier le ruban à l'endroit où elle pointe, si elle veut lire ou modifier un autre endroit, il faut déplacer le ruban dans un sens ou dans l'autre.
Nous pourrions avoir le même raisonnement pour un arbre. Un zipper d'arbre est :
Ainsi, ces informations permettent de connaitre exactement la structure complète de l'arbre, du point de vue du nœud pointé.
Revenons aux listes, et implémentons les principales fonctions du zipper de liste.
Créer un nouveau répertoire pour ce TP, puis dans ce répertoire créer un fichier zipperList.ml. Dans ce fichier, proposer un type pour les zipper de listes.
Écrire une fonction remplaçant l'élément pointé par la tête par une autre valeur.
Pour pouvoir lire et modifier d'autres éléments, il faut pouvoir se déplacer dans la liste représentée par le zipper. Pour cela, nous codons des fonctions élémentaires nous permettant de nous déplacer vers l'élément d'à-côté, s'il existe. Nous voulons aussi pouvoir tester si une extrémité de la liste est atteinte. Coder les fonctions suivantes.
On peut facilement créer une liste infinie en OCaml par une définition récursive :
Utiliser cette possibilité pour ajouter une fonction créant un zipper représentant une liste infinie des deux côtés, mais dont tous les éléments ont la même valeur.
Créer dans le même répertoire que zipperList.ml un fichier zipperList.mli.
Écrire l'interface de toutes les fonctions du zipper dans ce nouveau fichier :
Ajouter un fichier .merlin dans votre répertoire, contenant ;
Puis compiler votre module de zipper :
Corriger les éventuelles erreurs. Vérifier ensuite le contenu du répertoire courant et du sous-répertoire _build/.
Si vous avez le temps à la fin du TP, ajouter les fonctions suivantes aux zippers de listes :
Un algorithme sur un zipper de liste peut être décrit comme une succession d'opérations, par exemple : aller à droite, lire la valeur en tête, écrire telle valeur, aller à gauche, aller à gauche, etc. Dans une telle description, à aucun moment nous n'avons eu à mentionner explicitement le zipper lui-même, nous avons seulement parler de comment le manipuler.
Les monades d'états sont des interfaces permettant de décrire des suites d'opérations affectant un état (ici le zipper), sans mentionner l'état directement. Ils sont utilisés notamment en programmation fonctionnelle pour simuler des références ou permettre de retrouver un style de programmation proche des langages impératifs. Ici nous les utilisons pour simplifier la manipulation du ruban.
Récupérer les fichier :
Les ajouter à votre répertoire. Compiler stateMonad.cmo et stateMonadExample.native. Puis exécuter ce dernier.
Prenez connaissance de l'interface donnée dans stateMonad.mli, et familiarisez-vous avec les exemples de stateMonadExample.ml, par exemple en les modifiant.
Pour créer une monade sur un état de type nom_type, il suffit de passer l'identifiant de ce type au foncteur Make de StateMonad :
Ici on veut créer un zipper de liste paramêtré par un type element. Nous voulons donc écrire un foncteur prenant un type Elt.t en argument (tout comme StateMonad.Make), et retournant une interface de monade d'état avec state = Elt.t zipper.
Recopier le code suivant, et expliquer pourquoi c'est ce que nous voulons.
Si nous en restons là, tout manipulation du zipper nous demanderait de passer par les fonctions get ou apply. Nous allons donc redéfinir les fonctions de l'interface de zipper pour pouvoir les utiliser facilement avec le zipper. Pour cela, il suffit de transformer les fonctions déjà écrites. Il y a deux cas :
Nous devons donc écrire une fonction lift respectant :
qui récupère le zipper, calcule le résultat et le retourne. Écrire cette fonction lift.
Compléter le foncteur MakeMonadic, pour obtenir cette interface. Ajouter cette interface dans zipperList.mli.
Les machines de Turing sont un modèle de calcul, qui permet en informatique théorique d'étudier ce qui peut ou ne peut pas être calculer, et si oui avec quelle efficacité. Nous allons simplement montrer comment simuler une machine de Turing grâce à un zipper de liste.
Une machine de Turing peut se décrire par un automate à états finis munis d'un ruban. Le ruban est une sorte de tableau infiniment long, découpé en cases successives pouvant chacune contenir un symbole parmi une liste finie de symboles possibles. La machine dispose d'une tête de lecture disposée sur une case du ruban. Elle peut lire ou écrire sur cette case, se déplacer d'une case à droite ou d'une case à gauche. Naturellement le zipper va nous servir à coder le ruban.
Une machine de Turing s'exécute par étapes, appelées transitions. Pendant une transition, la machine lit le symbole se trouvant sous la tête de lecture, puis, selon l'état dans lequel elle se trouve et le caractère lu :
On pourrait donc résumer les transitions par une fonction :
Les transitions s'enchaînent jusqu'à atteindre l'état spécial Arrêt.
Nous proposons de simuler au choix :
Le second devrait être plus simple à coder.
Dans un nouveau fichier d'extension .ml, proposer un type pour les symboles du ruban et un type pour les états.
Pour l'additionneur, le ruban contiendra des cases soit vides, soit contenant deux bits à additionner, soit le résultat de l'addition. La machine a deux états en plus de Arrêt: AvecRetenu et SansRetenu.
Pour le busy-beaver, le ruban contient des 1 et des 0 uniquement (pas de cases vides), et possède des états A, B, C, D et Arrêt.
Pour le busy-beaver, le ruban initial est composé uniquement de 0 . Pour l'additionneur, seules les cases sous la tête et à sa droite sont non-vides, et contiennent des paires de deux bits : la $k^\textrm{e}$ paires à droite contient les bits de poids $k$ des deux entiers à additionner.
Écrire un valeur calculant le ruban initial de votre machine.
Appliquer le foncteur StateMonad.MakeMonadic pour obtenir un module Tape de monade dont l'état est un ruban pour votre machine.
Écrire la fonction de transition de votre machine :
Pour cela, utiliser Tape.read, Tape.write, Tape.step_right, etc, et les opérateurs infixes de Tape
Voici le tableau des transitions pour busy-beaver :
État initial | Symbole lu | Symbole Écrit | Déplacement | Nouvel état |
---|---|---|---|---|
A | 0 | 1 | Droite | B |
A | 1 | 1 | Gauche | B |
B | 0 | 1 | Gauche | A |
B | 1 | 0 | Gauche | C |
C | 0 | 1 | Droite | Halte |
C | 1 | 1 | Gauche | D |
D | 0 | 1 | Droite | D |
D | 1 | 0 | Droite | A |
Et celui pour l'additionneur
État initial | Symbole lu | Symbole Écrit | Déplacement | Nouvel état |
---|---|---|---|---|
AvecRetenu | $(0,0)$ | 1 | Gauche | SansRetenu |
AvecRetenu | $(1,0)$ | 0 | Gauche | AvecRetenu |
AvecRetenu | $(0,1)$ | 0 | Gauche | AvecRetenu |
AvecRetenu | $(1,1)$ | 1 | Gauche | AvecRetenu |
SansRetenu | $(0,0)$ | 0 | Gauche | SansRetenu |
SansRetenu | $(1,0)$ | 1 | Gauche | SansRetenu |
SansRetenu | $(0,1)$ | 1 | Gauche | SansRetenu |
SansRetenu | $(1,1)$ | 0 | Gauche | AvecRetenu |
AvecRetenu | Vide | 1 | Gauche | Halte |
SansRetenu | Vide | Vide | Gauche | Halte |
(dans tous les autres cas, écrire 0 , se déplacer à gauche et passer dans l'état Halt).
Depuis le ruban initial et un état initial, les transitions sont répétés jusqu'à atteindre l'état Arrêt. Écrire une fonction prenant l'état initial et simulant l'exécution de la machine de Turing.
Nous pouvons aussi en proposer une qui calcule aussi le nombre de transitions effectuées :
Utiliser run avec le ruban initial pour provoquer l'évaluation de la machine de Turing. Afficher le ruban produit (ou plutôt une partie finie du ruban).
Il serait intéressant d'afficher le ruban et l'état courant avant chaque transition, ainsi que le déplacement relatif de la tête de lecture depuis le début de l'évaluation. C'est possible en utilisant une autre monade d'état pour stocker le déplacement et le nombre de transitions effectuées par exemple, et faire le produit de cette monade avec la monade de ruban, grâce au foncteur StateMonad.Product