Cours 5

Htmligure
Version & licenses
Creative Commons License

Combinateurs d'écriture

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Conversion en chaînes de caractères.

Le prochain exemple concerne la conversion de valeurs en chaînes de caractères. Dans QCheck, le module Print définit des combinateurs pour écrire facilement des fonctions to_string. Voyons comment construire un tel module.

Le type principal est celui des fonctions de conversion. Il est bien sûr paramétré par le type de ce qui est converti : c'est donc un consommateur. On le définit très simplement, comme une fonction vers string :

  1. type 'value t = 'value -> string

On peut alors définir des convertisseurs pour les types de bases :

  1. let int = string_of_int
  2. let float = string_of_float
  3. let bool b = if b then "true" else "false"
  4. let char = String.make 1
  5. let string str = str

Puis on ajoute des combinateurs pour pouvoir construire des convertisseurs sur des types plus complexes :

  1. let pair fst_to_string snd_to_string (first,second) =
  2. String.concat ""
  3. ["("; fst_to_string first; ","; snd_to_string second; ")"]

  4. let list elt_to_string elts =
  5. elts
  6. |> List.map elt_to_string
  7. |> String.concat ";"
  8. |> fun str -> "[" ^ str ^ "]"

  9. let comap fct to_string value =
  10. to_string (fct value)

  11. let (=|>) = comap

On peut ainsi obtenir facilement un fonction de conversion pour des types assez simples :

  1. let int_int_pair_list_to_string : (int * int) list -> string =
  2. list (pair int int)
  3. let float_list_int_pair_to_string : (float list * int) -> string =
  4. pair (list float) int

Cela reste relativement limité. Si on reprend l'exemple des personnes, pour obtenir un formattage contenant le nom et l'âge d'une personne, on ne peut pas utiliser directement les combinateurs pour écrire une fonction de conversion, notamment parce que nous n'avons rien pour ajouter des chaînes de caractères brutes, ni pour imprimer plusieurs choses. Ajoutons donc un combinateur constant (il construit toujours la même chaîne de caractères), et un opérateur de concaténation :

  1. let cst : string -> 'value t = fun str value -> str

  2. let (++) : 'value t -> 'value t -> 'value t =
  3. fun to_string1 to_string2 value -> to_string1 value ^ to_string2 value

On peut maintenant écrire :

  1. let person_to_string =
  2. cst "{ Name: " ++ (get_name =|> string) ++ cst ", "
  3. ++ cst "age: " ++ (get_age =|> int) ++ cst " }"

On arrive a peu près au niveau de ce que permet de faire sprintf, mais en plus modulaire puisque nous avons la possibilité de créer nos propres formattages pour n'importe quel type; nous ne sommes pas limité aux types de bases.

On va ajouter un dernier combinateur. Imaginons que chaque personne possède une liste d'enfants, et qu'on souhaite aussi afficher la descendance de chaque personne qu'on affiche (heureusement, cette relation de filiation est acyclique, donc on n'affiche ainsi qu'un nombre fini de personnes). On voudrait écrire :

  1. let rec person_to_string =
  2. cst "{ Name: " ++ (get_name =|> string) ++ cst ", "
  3. ++ cst "age: " ++ (get_age =|> int) ++ cst ", "
  4. ++ cst "children: " ++ (get_children =|> list person_to_string) ++ " }"

Facile, mais faux. En effet, on ne peut pas définir de telles récursions : tout appel récursif doit se faire soit dans une abstraction (dans le corps d'une fonction), soit dans un constructeur. Pour pouvoir faire cela, il nous faut un combinateur en plus : le combinateur de point-fixe, qui permet d'exprimer des récursions.

  1. let rec fix : ('value t -> 'value t) -> 'value t =
  2. fun f value -> f (fix f) value

  3. let person_to_string =
  4. fix @@ fun person_to_string ->
  5. cst "{ Name: " ++ (get_name =|> string) ++ cst ", "
  6. ++ cst "age: " ++ (get_age =|> int) ++ cst ", "
  7. ++ cst "children: " ++ (get_children =|> list person_to_string) ++ cst " }"

  8. let ana =
  9. { name = "Ana";
  10. age = 76;
  11. children =
  12. [ { name = "Bob";
  13. age = 45;
  14. children =
  15. [ { name = "Claire"; age = 19; children = [] };
  16. { name = "Daniel"; age = 15; children = [] } ]
  17. };
  18. { name = "Éric";
  19. age = 36;
  20. children =
  21. [ { name = "Fanny"; age = 4; children = [] } ]
  22. }
  23. ]
  24. }

On a alors :

  1. let _ = person_to_string ana

  2. - : string = "{ Name: Ana, age: 76, children: [{ Name: Bob, age: 45,
  3. children: [{ Name: Claire, age: 19, children: [] };{ Name: Daniel,
  4. age: 15, children: [] }] };{ Name: Éric, age: 36, children: [{ Name:
  5. Fanny, age: 4, children: [] }] }] }"

Le module Format.

L'exemple que nous venons de voir, bien qu'il soit déjà relativement puissant, manque encore un peu de fonctionnalité. Par exemple, on ne peut pas gérer l'indentation convenablement, ni faire de la justification de texte.

Par ailleurs, ces combinateurs ne sont pas très efficaces, en particulier parce ce qu'ils manipulent les chaînes en faisant des concaténations dès que possible. Il crée donc beaucoup de chaînes intermédiaires et cela a un coût. Il serait plus intéressant de représenter les convertisseurs comme convertissant vers une structure intermédiaire, typiquement un arbre de chaînes de caractères, puis d'avoir une fonction convertissant ces arbres en une seule chaîne de caractères. Plus généralement, si on veut écrire cette chaîne dans un fichier ou en sortie, ce n'est pas la peine de la construire. Ainsi, passer par une représentation intermédiaire permettrait d'à la fois pouvoir gérer des sorties variées efficacement, et de permettre du formattage de texte avec indentation, justification et alignement des textes.

En fait c'est exactement ce que permet de faire le module Format de la librairie standard. Au premier abord, cela ressemble au module Printf.

  1. let print_time () =
  2. let open Unix in
  3. let tm = gmtime (gettimeofday ()) in
  4. Format.printf "It is %2d:%2d:%2d.\n%!"
  5. tm.tm_hour
  6. tm.tm_min
  7. tm.tm_sec

Mais Format permet aussi de créer des boîtes, avec "@[" et "@]", pour grouper des éléments que Format essaie autant que possible de présenter ensemble (sur une même ligne). Le motif "@ " permet d'insérer un espacement pouvant être utilisé comme retour à la ligne par l'algorithme de formattage, et "@;" indique la possibilité d'ajouter un retour à la ligne. Par ailleurs, on peut formatter des expressions récursivement avec le motif "%a" qui demande un argument supplémentaire : la fonction de formattage à utiliser en plus de l'argument à formatter.

Pour l'exemple des personnes, cela donne :

  1. let rec person_formatter fmt person =
  2. let pp_sep fmt () = Format.fprintf fmt ";@ " in
  3. let format_children = Format.pp_print_list ~pp_sep person_formatter in
  4. Format.fprintf fmt
  5. "{@[<hv 2>@ name: \"%s\";@ age: %d;@ children:@[<b 2>@ [%a]@]@]@;}"
  6. (get_name person)
  7. (get_age person)
  8. format_children (get_children person)

  9. let person_to_string person =
  10. Format.printf "%a" person_formatter person

<hv 2> indique quel type de formattage est attendu pour le texte entre le "@[" juste devant et le "@]"correspondant. On obtient :

  1. let _ = person_formatter Format.std_formatter ana

  2. {
  3. name: "Ana";
  4. age: 76;
  5. children:
  6. [{
  7. name: "Bob";
  8. age: 45;
  9. children: [{ name: "Claire"; age: 19; children: [] };
  10. { name: "Daniel"; age: 15; children: [] }]
  11. };
  12. {
  13. name: "Éric";
  14. age: 36;
  15. children: [{ name: "Fanny"; age: 4; children: [] }]
  16. }]
  17. }

Bien sûr, les nombreuses possibilités de Format exigent une quantité de subtilités supérieure à notre petit prototype. La documentation de Format contient un tutoriel expliquant les différents concepts et donnant quelques exemples.

Retour au sommaire.