Cours 5

Htmligure
Version & licenses
Creative Commons License

Grammaires et analyse syntaxique

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Grammaires, écriture et lecture.

Considérons maintenant le type suivant, pour les arbres binaires :

  1. type 'elt bintree =
  2. | Leaf
  3. | Node of 'elt bintree * 'elt * 'elt bintree

On peut facilement écrire un formatteur pour ce type :

  1. let rec format_bin_tree elt_formatter fmt = function
  2. | Leaf ->
  3. Format.fprintf fmt "Leaf"
  4. | Node (left,root,right) ->
  5. Format.fprintf fmt
  6. "(@[<v 2>@ left: %a,@ root:%a,@ right:%a@])"
  7. (format_bin_tree elt_formatter) left
  8. elt_formatter root
  9. (format_bin_tree elt_formatter) right

  10. let format_int_tree = format_bin_tree Format.pp_print_int

On observe donc l'arbre et on utiliser deux formatteurs différents selon la nature de l'arbre, nœud ou feuille. Cette solution, toute valide qu'elle soit, n'utilise pas uniquement des combinateurs : nous sommes forcés de prendre l'arbre en argument et de l'observer. Soyons curieux, et bien que cela semble inutile à première vue, demandons-nous s'il est possible de faire une fonction de conversion sans référencer l'arbre ?

Reprenons le module de la section précédente, et ajoutons-y quelques fonctions, de sorte qu'on obtient le module suivant :

  1. module Print :
  2. sig
  3. type 'value t

  4. val to_string : 'any t -> 'any -> string

  5. val int : int t
  6. val float : float t
  7. val char : char t
  8. val bool : bool t
  9. val string : string t

  10. val cst : string -> 'any t

  11. val (++) : 'value t -> 'value t -> 'value t

  12. val comap : 'im t -> f:('value -> 'im) -> 'value t
  13. val (=|>) : 'im t -> ('value -> 'im) -> 'value t

  14. type 'value conditional
  15. val (||) : 'value conditional -> 'value t -> 'value t

  16. val optionnally : ('value -> 'im option) -> 'im t -> 'value conditional
  17. val ( =? ) : ('value -> 'im option) -> 'im t -> 'value conditional

  18. val pair : 'left t -> 'right t -> ('left * 'right) t
  19. val triple : 'left t -> 'middle t -> 'right t -> ('left * 'middle * 'right) t
  20. val list : 'elt t -> 'elt list t

  21. val fix : ('value t -> 'value t) -> 'value t

  22. end

Le principal ajout est le type conditional et les fonctions (||) et (=?). Nous allons voir par l'exemple comment ils s'utilisent. Ajoutons deux fonctions pour l'arbre binaire; ces deux fonctions sont des sortes d'accesseurs soit aux valeurs de la feuille (rien) soit au valeurs du nœud (un triplet).

  1. type 'elt bin_tree =
  2. | Leaf
  3. | Node of 'elt bin_tree * 'elt * 'elt bin_tree

  4. let observe_leaf = function
  5. | Leaf -> Some ()
  6. | Node _ -> None

  7. let observe_node = function
  8. | Leaf -> None
  9. | Node (left,root,right) -> Some (left,root,right)

Maintenant :

  1. let bin_tree_to_string elt =
  2. let open Print in
  3. let tree =
  4. fix @@ fun tree ->
  5. observe_node =? cst "Node " ++ triple tree elt tree
  6. || observe_leaf =? cst "Leaf"
  7. || cst "not possible"
  8. in
  9. Print.to_string tree

  10. let () =
  11. Node (Node (Leaf,1,Leaf), 2, Node (Node (Leaf,3,Leaf),4,Leaf))
  12. |> bin_tree_to_string Print.int
  13. |> Printf.printf "%s"

  14. (* result:
  15. Node (Node (Leaf, 1, Leaf), 2, Node (Node (Leaf, 3, Leaf), 4, Leaf))
  16. *)

En fait ...=?... || ... s'utilise comme un opérateur ternaire, le type conditional est là uniquement pour pouvoir attribuer un type à la première partie. On comprend donc que le premier terme permet de trouver une représentation de l'objet à écrire, mais peut échouer à le représenter. Le deuxième terme définit comment écrire la représentation, et le troisième terme est la solution de repli : comment écrire si l'objet n'est pas ainsi représentable.

À quoi bon ? on est ici dans le domaine de l'astuce et cette solution n'apporte pas grand chose de plus par rapport à prendre l'objet et faire un filtrage par motif, comme avec Printf. Certes, mais cette solution met en évidence la grammaire des chaînes de caractères encodant des arbres :

  1. tree :=
  2. "Node" "(" tree "," elt "," tree ")"
  3. | "Leaf"

Et comme on a des actions associés à chaque règle de la grammaire lorsqu'on écrit un analyseur syntaxique, ici on a aussi une sorte d'action (inversée, une observation du coup) pour chaque règle : observe_node et observe_leaf. Il n'y a plus qu'une question à se poser : peut-on utiliser la même technique, non pour écrire, mais pour lire ? Pourrait-on avoir un lecteur d'arbre dont le programme soit le suivant ?

  1. let make_node (left,root,right) = Node (left,root,right)
  2. let make_leaf () = Leaf

  3. let bin_tree_to_string (elt: 'elt Read.t) : 'elt bin_tree Read.t =
  4. let open Read in
  5. fix @@ fun tree ->
  6. (make_node *> cst "Node " ++ triple tree elt tree)
  7. || (make_leaf *> cst "Leaf")

Affirmatif, et c'est ce que nous allons étudier par la suite.

Notons que la lecture de séquence de caractères peut être faite avec Scanf, qui est assez puissant pour par exemple lire des arbres :

  1. let rec read_tree read_elt in_chan =
  2. let create_node left root right = Node (left root right) in
  3. try
  4. Scanf.bscanf in_chan " Node (%r, %r, %r)"
  5. (read_tree read_elt)
  6. read_elt
  7. (read_tree read_elt)
  8. create_node
  9. with Scanf.Scan_failure _ -> Scanf.bscanf in_chan " Leaf" Leaf

Retour au sommaire.