Pretty printing et bibliothèque StructuredFormat

Cet article ne fait pas partie de l'introduction à F# : c'est un article indépendant, qui présente une fonctionnalité peu connue de F#. Si vous débutez, vous pouvez sauter sans problème ce billet.

Introduction

F# possède plusieurs outils pour faire du pretty printing et de l'affichage générique de valeurs. Cela consititue un très gros avantage pour F# sur OCaml. StructuredFormat est la bibliothèque d'affichage utilisée dans le mode interactif (pour afficher les valeurs). Il est possible de personnaliser l'affichage de n'importe quel type en implémentant l'interface IFormattable. Il suffit de fournir la méthode GetLayout pour indiquer la manière d'afficher la valeur. La bibliothèque StructuredFormat possède un certain nombre de fonctions et d'opérateurs pour simplifier l'écriture.

Pour faire appel au moteur d'affichage, il suffit d'utiliser l'une des fonctions suivantes :

  • print_any ('a -> unit) : affiche la valeur sur la sortie standard ;
  • prerr_any ('a -> unit) : affiche la valeur sur la sortie d'erreur ;
  • output_any (out_channel -> 'a -> unit) : affiche la valeur sur le canal spécifié ;
  • printf "%A" ('a -> unit) : comme print_any, mais permet de modifier le format ;
  • any_to_string ('a -> string) : renvoie la valeur dans une chaine de caractère

Le layout définit comment seront agencés les différents éléments, où peuvent (ou doivent) être les espaces et l'indentation. Les types de base peuvent être laissés non formatés, pour laisser le moteur décider par lui-même (en utilisant les informations liées à la langue et la culture). L'affichage peut enfin être personnalisé : largeur, profondeur (par exemple, on peut vouloir n'afficher que les 10 premiers éléments d'une liste très longue (voire infinie)), culture, etc.

Fonctions

Je vous conseille de regarder la documentation de sformat et de voir la liste des fonctions disponibles. Pour convertir une valeur simple en Layout, vous pouvez utiliser les fonctions suivantes :

  • objL convertit un objet en Layout. C'est la méthode par défaut, elle est recommandée pour tous les nombres et les types de base. Vous devrez souvent commencer par convertir la valeur en objet (fonction box).
  • wordL, sepL, leftL et rightL convertissent une chaine de caractère en Layout. wordL est la conversion de base, a plus fréquente. leftL (et rightL) est utilisée quand la chaine se comporte comme une parenthèse gauche (ou droite) : il faut une espace à droite (ou gauche), mais pas de l'autre côté. sepL est utilisée pour les séparateurs habituels, qui ne nécessitent pas d'espace.
  • listL, spaceListL, commaListL... sont des fonctions très pratiques pour afficher des listes (séparées par des espaces, des virgules, etc.).

Plusieurs opérateurs sont disponibles (et vous vous rendrez compte une fois de plus à quel point la définition d'opérateurs est vital, et vous serez à nouveau heureux de ne pas coder dans un langage préhistorique) pour regrouper les éléments. Vous décidez s'ils sont insécables, sécables ou séparés (par un retour à la ligne). Vous pouvez également décider de l'indentation.

  • $$ : insécables
  • ++ : sécables (sans indentation)
  • -- : sécables (indentation de 1)
  • --- : sécables (indentation de 2)
  • @@ : séparé (sans identation)
  • @@- : séparé (indentation de 1)
  • @@-- : séparé (identation de 2)

Faites attention en combinant ces opérateurs. Les règles de priorité ne sont pas toujours intuitives, par exemple ++ est plus prioritaire que $$, ce qui peut être déroutant. Voici un exemple pratique pour l'affichage d'un arbre :

[fsharp]
let get_layout (env: #IEnvironment) (e: #IFormattable) = e.GetLayout(env)

type ast =
 | Val of int
 | Var of string
 | BinOp of binop * ast * ast
 | UnOp of unop * ast
with
   interface StructuredFormat.IFormattable with
      member x.GetLayout(env) =
         match x with
          | Val i -> objL (box i)
          | Var s -> wordL s
          | BinOp (Custom (s, _), t1, t2) ->
               wordL s ++ get_layout env t1 ++ get_layout env t2
          | BinOp (b, t1, t2) ->
               (leftL "(" $$ get_layout env t1) ++ get_layout env b ++
                 (get_layout env t2 $$ rightL ")")
          | UnOp (b, t1) ->
               get_layout env b $$ get_layout env t1
   end
...

La fonction get_layout définie si dessus est facultative, elle permet surtout d'éviter d'avoir à mettre des annotations de type. Pour tester votre affichage, je vous recommande d'essayer en mode interactif. Vous pouvez modifier la valeur fsi.PrintWidth pour mieux vous rendre compte du comportement de l'affichage, notamment concernant les retours à la ligne.

Indentation

Voici deux exemples pour expliquer comment indenter un "if" dans un langage.

Code source
[fsharp]
 | If (cond, exp1, exp2) ->
      ((wordL "if" $$ get_layout env cond $$ wordL "then")
       @@- (get_layout env exp1))
       @@ wordL "else"
       @@- (get_layout env exp2)
Sortie
[fsharp]
if exp then
 1
else
 2
Code source
[fsharp]
 | If (cond, exp1, exp2) ->
      (wordL "if" $$ get_layout env cond) --
       aboveL
         (wordL "then" -- (get_layout env exp1))
         (wordL "else" -- (get_layout env exp2))
Sortie
[fsharp]
if exp then 1
       else 2

Exemple complet

J'ai écrit un exemple complet qui utilise un pretty printer sur un arbre. L'exemple met aussi en valeur plusieurs fonctionnalités, telles que la surcharge d'opérateurs, la définition de nouveaux opérateurs et les manipulations d'arbre. Le code contient un arbre qui décrit des expressions arithmétiques. L'AST peut être affiché ou simplifié (en utilisant des règles arithmétiques, du genre x * 0 = 0, etc.).

Code source