[F#] [Tuto] Pretty printing et bibliothèque StructuredFormat
La version anglaise de ce texte peut être trouvée là : http://laurent.le-brun.eu/fsharp-pretty-printer
Ce billet peut aussi être consulté en français sur mon site : http://laurent.le-brun.eu/fsharp-pretty-printer-fr
Introduction
F# possède plusieurs outils pour faire du pretty printing et de l'affichage générique de valeurs. Cela constitue 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 lecanal spécifié (souvent utilisé avec un printf "%a") ;
- 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 :
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.
| If (cond, exp1, exp2) -> ((wordL "if" $$ get_layout env cond $$ wordL "then") @@- (get_layout env exp1)) @@ wordL "else" @@- (get_layout env exp2)Sortie
if exp then 1 else 2Code source
| If (cond, exp1, exp2) -> (wordL "if" $$ get_layout env cond) -- aboveL (wordL "then" -- (get_layout env exp1)) (wordL "else" -- (get_layout env exp2))Sortie
if exp then 1 else 2
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 de l'exemple complet, en F#
- LLB
- 22:28
- > Lien permanent
- > Commentaires
- > Abus ?


![[Jeu] Ideo](images_/carre1.gif)
![[F#] [Tuto] Pretty printing et bibliothèque StructuredFormat](images_/carre3.gif)
