TD 8
-
Parser du JSON
- 1. Gestion des blancs
- 2. Reconnaître
null
- 3. Reconnaître les booléens
- 4. Tout sauf certains caractères
- 5. Parser une chaîne
- 6. Reconnaître les chaînes JSON
- 7. Concaténation de résultats
- 8. Chaine optionnelle
- 9. Parser des nombres
- 10. Reconnaître les entiers
- 11. Reconnaître les flottants
- 12. Reconnaître du JSON
- 13. Itérations avec virgules
- 14. Reconnaître les listes
- 15. Parser des paires clé-valeur
- 16. Reconnaître des objets
- 17. Reconnaître un caractère échappé
- 18. Reconnaître des caractères échappés
- 19. Chaînes contenant des échappements
- Transformation de fichiers JSON
Parser du JSON
JSON est un format d’échange de données structurées. Ce format a l’avantage d’être très simple à utiliser. En effet, les données sont représentées dans un format textuel qui utilise une syntaxe compatible avec celle du langage de programmation JavaScript.
Dans la suite, nous ne considérerons que les constructions principales de JSON.
Le modèle de données que l’on considère est le suivant :
1. Gestion des blancs
En dehors des chaînes de caractères, les caractères blancs n’ont pas
d’importance dans un document JSON. On va donc écrire une fonction
passeBlancs
qui, prenant un parseur p
en argument, renvoie un parseur
qui :
- exécute
p
; - passe tous les blancs présents en début de la chaîne qui reste à analyser ;
- retourne la valeur calculée par
p
.
On va utiliser la fonction isSpace :: Char -> Bool
qui reconnaît les
caractères blancs.
Désormais, lorsque nous analyserons un élément d’un document JSON, nous prendrons la convention de passer les blancs qui le suivent. On ne le fera que pour les parsers qui construisent un élément JSON.
2. Reconnaître null
On va reconnaître la chaîne de caractères "null"
pour construire la valeur
JSON correspondante.
3. Reconnaître les booléens
Pour les booléens, c’est un peu différent, mais pas tant que ça. Il faut toujours reconnaître une chaîne de caractères, mais on en a deux possibles.
On peut imaginer quelque chose comme
Mais ça ne renvoie pas une valeur de type JSON
.
Il faut qu’on applique un constructeur à cette valeur.
Il faut aussi qu’on passe les blancs.
Une solution (ce n’est pas la seule, mais elle évite de répéter du code) serait :
Ici, on utilise le fait que notre module Parser
soit alternatif.
4. Tout sauf certains caractères
Quand on va parser des chaînes de caractères, il va falloir que l’on sache reconnaître tous les caractères qui sont entre deux guillemets, mais qui ne sont pas des guillemets.
Pour s’aider, on va faire un parseur toutSauf
, qui étant donné une
chaîne de caractères, accepte tout caractère qui n’est pas dans cette
liste.
5. Parser une chaîne
On va écrire un parseur qui reconnaît tout le texte inclus entre des guillemets.
6. Reconnaître les chaînes JSON
Maintenant qu’on sait reconnaître une chaine de caractères, on veut la convertir en valeur JSON.
7. Concaténation de résultats
Si on a deux parseurs qui renvoient chacun une liste d’éléments du même type, on va définir la concaténation entre les résultats.
On exécute donc p1
, puis p2
et on concatène les deux résultats.
8. Chaine optionnelle
On va définir un parseur qui reconnaît une chaîne si elle est bien au début du texte à analyser et qui renvoie la chaîne vide sinon.
9. Parser des nombres
On va écrire un parseur qui va reconnaître une chaîne de caractères
composée uniquement de chiffres. On utilise pour ça la fonction
bien nommée isDigit
.
10. Reconnaître les entiers
Un entier, c’est une séquence non vide de chiffres, précédée ou non
du signe "-"
. On veut parser la chaîne correspondante et la
renvoyer sous la forme d’une valeur JSON.
11. Reconnaître les flottants
Même principe, mais on veut reconnaître des flottants simplifiés. Ils sont écrits sous la forme d’un nombre suivi d’un point suivi d’un nombre. Ils peuvent aussi être négatifs.
Pour la suite, on va définir 3 parseurs mutuellement récursifs :
-
parseArray
, qui va parser des listes JSON -
parseObject
, qui va parser des objets JSON -
parseJson
, qui va parser n’importe quel JSON
12. Reconnaître du JSON
On va vouloir renvoyer la valeur JSON quelle qu’elle soit.
13. Itérations avec virgules
Dans les listes et les objets, on a des éléments qui sont séparés par des virgules. On va donc définir des parseurs qui permettent de reconnaître des listes d’éléments séparés ainsi.
On commence par reconnaître une virgule et la jeter.
Ensuite, on va définir un parseur qui va fonctionner comme some p
,
mais qui avant chaque application de p
(après la première), passe
la virgule.
On peut étendre ce parseur pour reconnaître les listes vides également.
14. Reconnaître les listes
On va maintenant pouvoir parser des listes. Une liste en JSON est une suite d’éléments séparés par des virgules, le tout dans des crochets.
On peut donc tout simplement faire :
15. Parser des paires clé-valeur
Pour les objets, c’est un peu plus compliqué : il faut qu’on reconnaisse une paire
constituée d’une clé (un String
) et d’une valeur (un JSON
), séparée par un ':'
.
On écrit donc un petit parseur pour s’aider :
16. Reconnaître des objets
On peut maintenant reconnaître facilement des objets, qui sont une suite séparée par des virgules de paires clé-valeur, le tout enclos dans des accolades.
17. Reconnaître un caractère échappé
Le format des chaînes de caractères que nous avons parsé est simpliste :
nous voulons pouvoir mettre des '"'
dans les chaînes.
Suivant le format utilisé dans beaucoup de langages (Haskell, Java, C, JavaScript, etc.),
JSON utilise '\'
comme caractère d’échappement, c’est-à-dire que les deux caractères '\"'
dans la représentation d’une chaîne signifient que la chaîne contient un '"'
.
Il faut du coup aussi représenter '\'
par '\\'
.
Cette mécanique est aussi utile pour les retours à la ligne, codés '\n'
, etc.
On va coder un parseur qui permet de remplacer une suite de caractères échappés par le bon caractère. Pour ça, on va lui donner une paire dont le premier élément est le caractère suivant le slash, et le deuxième est le caractère par lequel le remplacer.
18. Reconnaître des caractères échappés
On va avoir une liste des échappements possibles, comme par exemple :
Pour se simplifier le travail, on va écrire un parseur qui va reconnaître tous les échappements de la liste. Comme on a déjà fait le travail pour un seul caractère, et qu’on va vouloir faire une alternative entre les caractères, on peut tout simplement replier la liste.
19. Chaînes contenant des échappements
On va maintenant mettre à jour notre parseur de chaîne pour qu’il accepte les échappements.
On va donc reconnaître tout sauf un guillemet ou un slash ou bien un échappement.
Transformation de fichiers JSON
Tout ça, c’est bien, mais on prend une chaîne de caractères en entrée. Dans la vraie vie, le texte est souvent contenu dans un fichier. On peut aussi vouloir écrire notre représentation sous la forme d’un texte JSON.
1. De JSON à JSON
On va écrire une fonction jsonToString
qui va prendre une valeur JSON et la
transformer en chaîne de caractères JSON valide, soit l’inverse du parseur.
Pour avoir une représentation plus compacte, on ne va pas mettre de retour à la ligne et limiter les espaces entre les éléments.
On utilisera show
et intercalate
(join
en Python)
2. De JSON à YAML
On va maintenant convertir notre valeur JSON en valeur YAML, qui est un autre format d’échange de données.
On va utiliser show
et unlines
, qui concatène des chaînes avec des retours à la ligne.
Comme on n’a plus de séparateur comme en JSON, on va utiliser une fonction auxilliaire pour afficher le texte à la bonne indentation.
3. Transformation de fichiers
On veut maintenant écrire une fonction qui va lire un fichier JSON, le parser, le convertir en chaîne et l’écrire dans un nouveau fichier.
Notre fonction a donc le type jsonFileConvert :: (JSON -> String) -> FilePath -> FilePath -> IO ()
.
On va utiliser les fonctions :
-
readFile :: FilePath -> IO String
et writeFile :: FilePath -> String -> IO ()
4. JSON mini et JSON YAML
On peut utiliser cette fonction pour construire des fonctions qui écrivent une version simplifiée du JSON ou une version YAML.