Programmation Avancée
TP7: transformations de monades et parsing monadique
On définit la notion de monade ainsi que la monade identité:
module type MONAD = sig type 'a m val return : 'a -> 'a m val bind : 'a m -> ('a -> 'b m) -> 'b m end module Id : MONAD with type 'a m = 'a = struct type 'a m = 'a let return x = x let bind m f = f m end
Exercice 1: transformations de monades
Pour cet exercice,
il n'est pas nécessaire d'implémenter la fonction lift
des transformations de monades: on se
contentera d'un foncteur de MONAD
vers MONAD
.
Question 1
On a vu que le type list
permet de construire une monade pour le calcul
non-déterministe. La construction peut être généralisée en une transformation
de monade, permettant d'étendre une monade sur 'a m
en
une monade sur 'a list m
, ajoutant la notion de non-déterminisme
aux calculs représentés.
Implémenter la transformation
ListT : functor (M:MONAD) -> MONAD with type 'a m = 'a list M.m
.
On pourra s'aider d'une fonction auxiliaire
mconcat : 'b list M.m list -> 'b list M.m
qui exécute dans M
tous les calculs de la liste passée en argument, et renvoie dans
M
la concaténation des listes résultantes.
Question 2
La monade de lecture permet de représenter des calculs qui lisent un flux de données en le consommant graduellement. Cela permet par exemple de modéliser la lecture d'un fichier, ou du canal d'entrée standard, telle qu'effectuées avec l'API Unix. Dans ce mode de lecture, si on compose deux calculs et que le premier lit trois caractères, la première opération de lecture du second calcul devra lire le quatrième caractère du flux initial.
On peut construire la monade de lecture from scratch sur le
type 'a m = char list -> char list * 'a
, mais on va ici faire
les choses de façon plus modulaire:
implémenter la transformation
ReaderT : functor (M:MONAD) -> MONAD with type 'a m = char list -> (char list * 'a) M.m
.
Exercice 2: monade de parsing
La monade de parsing combine la monade de lecture (on lit un flux de tokens)
et la monade de non-déterminisme (par défaut, une grammaire est ambigue et
le parsing peut avoir plusieurs résultats).
Le type de la monade doit être char list -> (char list * 'a) list
.
Question 1
Construire la base de la monade Parsing
en
appliquant les transformations précédentes sur la monade Id
.
Question 2
Définir dans cette monade les opérations suivantes:
val plus : 'a m -> 'a m -> 'a m val zero : 'a m val read : char m val eos : bool m
Le plus
correspond au choix non-déterministe entre deux calculs,
et zero
est l'échec. L'opération read
lit le premier char
du flux
d'entrée, échoue si le flux est déja terminé. L'opération eos
(end of stream) indique enfin si le flux est terminé.
Exercice 3: parsing
L'objectif, à partir de maintenant, est de ne plus jamais s'appuyer sur
la définition de 'a Parsing.m
, mais d'utiliser uniquement les opérations
élémentaires définies ci-dessus.
Nous allons définir des combinateurs de parsing qui permettent notamment de
construire, pour tout langage régulier L
, un parseur p(L)
de type char list m
tel que, pour tout préfixe w
du flux d'entrée,
p(L)
lit w
et renvoie (au moins) une fois w
ssi w
appartient à L
.
L'opération zero : char list m
correspond déja au langage vide selon
cette spécification, et plus
correspond à l'union de langages.
Question 1
Définir one : char list m
correspondant au langage contenant uniquement
le mot vide: one
ne doit donc rien lire et juste renvoyer []
.
Définir char : char -> char list m
tel que char c
reconnaisse le mot
restreint au caractère c
; le calcul doit donc vérifier qu'il y a un
caractère à lire, et qu'il s'agit bien de c
.
Définir concat : char list m -> char list m -> char list m
qui corresponde
à la concaténation de langages. Utiliser les opérations précédemment définies
pour dériver le parser reconnaissant une chaîne donnée:
string : string -> char list m
.
Tester:
let () = assert (List.length (string "fo" ['b';'a';'r']) = 0) ; assert (List.length (string "fo" ['f';'o';'o']) = 1) ; assert (List.length (string "fo" ['f';'a']) = 0)
Question 2
Définir star : char list m -> char list m
en suivant l'équation
A* = 1 + AA*
.
Il faudra faire attention aux récursions, et introduire si besoin une
notion de concaténation paresseuse qui ne calcule son second parseur
qu'une fois qu'on a obtenu un résultat du premier parseur.
Tester:
let (!) = star let (++) = plus let () = assert (List.length (!(char 'a' ++ char 'b') ['a';'b';'a']) = 4)
Quels résultats obtenez vous pour !(char 'a' ++ string "aa")
sur ['a';'a';'a']
?
Question 3
Un parseur ne doit pas seulement servir à reconnaître des (sous-)mots, mais
aussi à les structurer ou les interpréter. Nous allons donc arrêter
de considérer uniquement des objets de type char list m
.
Définir digit : int m
qui reconnaît les mots composés d'un seul chiffre
et renvoie leur valeur numérique. En déduire un parser expr : int m
qui reconnaît les expressions arithmétiques simples définies ci-dessous
et renvoie leur interprétation dans int
:
digit ::= ['0'-'9'] expr ::= digit | digit '+' expr
Que se passe-t-il si on écrit le même langage par une grammaire récursive à gauche?
On pourra enfin ajouter la multiplication en respectant les priorités
usuelles, du parenthésage, lire des entiers supérieurs à 9
, etc.
Références
Functional Pearl: Monadic parsing in Haskell de Graham Hutton et Erik Meijer.