Comment imprimer joliment (utiliser ``format'') ?

Contacter l'auteur Pierre.Weis@inria.fr

Fichier créé en novembre 1995.

Le module format propose une méthode d'impression enjolivée. Ce module implémente un moteur d'impression qui coupe ``bien'' les lignes (``bien'' signifie à-peu-près ici ``automatiquement et quand nécessaire'').

Table des matières:

Les principes

La coupure des lignes repose sur deux concepts:

Boîtes

Il y a 4 types de boîtes. (La plus communément utilisée est la boîte ``hov'', laissez tomber les autres types en première lecture.)

Donnons un exemple. Supposons que nous voulions écrire 10 caractères avant la marge droite (qui indique qu'il n'y a plus de place sur la ligne courante). Je représente chaque caractère par une marque -, les ouvertures et fermetures de boîtes sont indiquées respectivement par [ et ], et b signifie une indication de coupure (blanc ou ``break'').

La sortie "--b--b--" est imprimée comme suit (le symbole b vaut la valeur de la coupure comme expliqué ci-après):

Impression des espaces

Les indications de coupure sont aussi utilisées pour imprimer des espaces (si la ligne n'est pas coupée quand l'indication de coupure est traitée, sinon le retour à la ligne sépare correctement les éléments à imprimer).

Vous donnez une indication de coupure en appelant print_break sp indent, où sp est l'entier qui indique le nombre d'espaces à imprimer.
Donc print_break sp ... signifie imprimer sp espaces ou aller à la ligne.

Par exemple, si l'on imprime "--b--b--" (où b est print_break 1 0, ce qui correspond à l'impression d'un espace), on obtient la sortie suivante:

De façon général, un programme qui utilise format, n'écrit pas d'espaces lui-même mais émet des indications de coupure. (Par exemple à l'aide de print_space () qui est synonyme de print_break 1 0 et écrit un espace ou déclenche une coupure de ligne.)

Indentation des lignes nouvelles

On dispose de deux moyens de fixer l'indentation des lignes:

Raffinement sur les boîtes ``hov''

Boîte ``hov'' tassante et boîte ``hov'' structurelle

Les boîtes ``hov'' se subdivisent en deux catégories au comportement légèrement différent en ce qui concerne les coupures qui interviennent après la fermeture d'une boîte dont l'indentation est différente de la boîte qui l'englobe. On distingue:

Différences entre boîte ``hov'' tassante et boîte ``hov'' structurelle

La différence de comportement entre la boîte ``hov'' tassante et la boîte ``hov'' structurelle (ou ``box'') est mise en évidence par la fermeture des boîtes et la fermeture des parenthèses en fin d'impression: avec la boîte ``hov'' tassante les boîtes et les parenthèses sont fermées sur la même ligne (si la place disponible le permet), tandis qu'avec la boîte ``hov'' structurelle chaque indication de coupure produira un saut de ligne. Prenons l'exemple de la sortie de "[(---[(----[(---b)]b)]b)]" où "b" représente une indication de coupure sans indentation supplémentaire (print_cut ()). Ainsi, si "[" représente l'ouverture de boîtes ``hov'' tassantes (open_hovbox), "[(---[(----[(---b)]b)]b)]" est imprimé ainsi:

(---
 (----
  (---)))
Si maintenant on remplace les boîtes ``hov'' tassantes par des boîtes ``hov'' structurelles (open_box), chaque indication de coupure placée avant chaque parenthèse fermante est susceptible de montrer la structure de boîte et produit donc une coupure; on obtient alors:
(---
 (----
  (---
  )
 )
)

Pratique de l'impression avec format

En écrivant vos fonctions d'impression, suivez les règles simples suivantes:

  1. Les boîtes doivent être ouvertes et fermées de façon cohérente (les appels à open_* et à close_box doivent être parenthésés).
  2. N'hésitez pas à ouvrir des boîtes.
  3. N'imprimez pas d'espaces vous-même, mais utilisez une indication de coupure (print_space ()). (Il est bien sûr quelquefois nécessaire d'imprimer des caractères ``espace'', ou blancs insécables, mais la plupart du temps un espace correspond plutôt à une indication de coupure.)
  4. N'imprimez jamais de retour à la ligne dans les chaînes de caractères: le moteur d'impression considèrera à juste titre ce retour chariot comme un caractère quelconque émis sur la ligne courante, ce qui dérangera complètement la sortie. Utilisez à la place des coupures de ligne: si celles-ci doivent se produire à tout coup, c'est que la boîte englobante doit être une boîte verticale!
  5. Donnez beaucoup d'indications de coupures, sinon l'imprimeur se retrouve dans une situation anormale (coincé sur la marge droite), où il essaie de faire de son mieux, ce qui n'est pas toujours très bon.
  6. N'essayez pas de forcer les coupures de ligne, laissez l'imprimeur le faire pour vous: c'est son travail.
  7. Terminez votre programme principal d'impression par un appel à print_newline (), qui vide les tables de l'imprimeur (et donc termine l'impression). (Notez que le système interactif le fait également à la fin de chaque phrase entrée.)

Utilisation de la fonction printf

Le module format vous propose une fonction générale de formattage à la printf. En plus des indications de format habituelles à la primitive printf, on dispose dans le format de caractères qui commandent ouvertures et fermetures de boîtes ainsi que l'émission d'indications de coupure de ligne.

Les indications spécifiques au moteur d'impression sont toutes introduites par le caractère @. À peu près toutes les fonctions du module format peuvent être appelées depuis un format de printf. Ainsi:

Par exemple
printf "@[<1>%s@ =@ %d@ %s@]@." "Prix TTC" 100 "Euros";;
Prix TTC = 100 Euros
- : unit = ()

Un exemple plus réaliste est donné plus bas.

Un exemple grandeur réelle

Voici un exemple complet: le plus petit exemple non trivial qu'on puisse imaginer, c'est-à-dire le $\lambda-$calculus :)

Le problème est donc d'imprimer les valeurs d'un type concret qui modélise un langage d'expressions qui définissent les fonctions et leur application à des arguments.

D'abord, je donne la syntaxe abstraite des lambda-termes, puis un analyseur lexical et un analyseur syntaxique pour ce langage:

type lambda =
   | Lambda of string * lambda
   | Var of string
   | Apply of lambda * lambda;;

(* L'analyseur lexical utilise le module genlex module de la bibliothèque *)
#open "genlex";;
let lexer = make_lexer ["."; "\\"; "("; ")"];;

(* L'analyseur syntaxique, à l'aide de streams *)
let rec exp0 = function
  | [< 'Ident s >] -> Var s
  | [< 'Kwd "("; lambda lam; 'Kwd ")" >] -> lam

and app = function
  | [< exp0 e; (other_applications e) lam >] -> lam

and other_applications f = function
  | [< exp0 arg; stream >] ->
      other_applications (Apply (f, arg)) stream
  | [<>] -> f

and lambda = function
  | [< 'Kwd "\\"; 'Ident s; 'Kwd "."; lambda lam >] ->
        Lambda (s, lam)
  | [< app e >] -> e;;

Essayons l'analyseur dans le système interactif:

#let parse_lambda s = lambda (lexer (stream_of_string s));;
parse_lambda : string -> lambda = <fun>
#parse_lambda "(\x.x)";;
- : lambda = Lambda ("x", Var "x")
Maintenant, j'utilise le module format pour imprimer les lambda-termes: je suis le squelette récursif de l'analyseur précédent pour écrire l'imprimeur, en insérant ça et là des indications de coupures et des ordres d'ouverture (et de fermeture) de boîtes:
#open "format";;

let ident = print_string;;
let kwd = print_string;;

let rec print_exp0 = function
  | Var s ->  ident s
  | lam -> open_hovbox 1; kwd "("; print_lambda lam; kwd ")"; close_box ()

and print_app = function
  | e -> open_hovbox 2; print_other_applications e; close_box ()

and print_other_applications f =
  match f with
  | Apply (f, arg) -> print_app f; print_space (); print_exp0 arg
  | f -> print_exp0 f

and print_lambda = function
 | Lambda (s, lam) ->
     open_hovbox 1;
     kwd "\\"; ident s; kwd "."; print_space(); print_lambda lam;
     close_box()
 | e -> print_app e;;
On obtient:
print_lambda (parse_lambda "(\x.x)");;
\x. x- : unit = ()

(Notez que les parenthèses sont traitées correctement par l'imprimeur print_lambda, qui émet le nombre minimum de parenthèses compatible avec une relecture correcte par l'analyseur syntaxique.)

print_lambda (parse_lambda "(x y) z");;
x y z- : unit = ()

print_lambda (parse_lambda "x y z");;
x y z- : unit = ()

Si vous utilisez cet imprimeur pour déboguer avec le système interactif, déclarez cet imprimeur avec install_printer, de telle manière que le système Caml l'utilise pour imprimer les valeurs de type lambda:

install_printer "print_lambda";;
- : unit = ()

parse_lambda "(\x. (\y. x y))";;
- : lambda = \x. \y. x y

parse_lambda "((\x. (\y. x y)) (\z.z))";;
- : lambda = (\x. \y. x y) (\z. z)

C'est une bonne méthodologie à suivre quand on utilise la trace du système interactif (en fait, dès que les données manipulées sont un peu complexe, je considère qu'il est indispensable de définir un imprimeur pour obtenir une trace lisible):

trace"lambda";;
La fonction lambda est dorénavant tracée.
- : unit = ()

parse_lambda
  "((\ident. (\autre_ident. ident autre_ident)) \
   (\Truc.Truc Truc)) (\machin. (machin machin) machin)";;
lambda <-- <abstr>
lambda <-- <abstr>
lambda <-- <abstr>
lambda <-- <abstr>
lambda <-- <abstr>
lambda <-- <abstr>
lambda --> ident autre_ident
lambda --> \autre_ident. ident autre_ident
lambda --> \autre_ident. ident autre_ident
lambda --> \ident. \autre_ident. ident autre_ident
lambda <-- <abstr>
lambda <-- <abstr>
lambda --> Truc Truc
lambda --> \Truc. Truc Truc
lambda --> (\ident. \autre_ident. ident autre_ident) (\Truc. Truc Truc)
lambda <-- <abstr>
lambda <-- <abstr>
lambda <-- <abstr>
lambda --> machin machin
lambda --> machin machin machin
lambda --> \machin. machin machin machin
lambda --> (\ident. \autre_ident. ident autre_ident) (\Truc. Truc Truc)
             (\machin. machin machin machin)
- : lambda =
 (\ident. \autre_ident. ident autre_ident) (\Truc. Truc Truc)
   (\machin. machin machin machin)

Exemple d'utilisation de printf

On utilise la fonction fprintf et toutes les fonctions d'impression prennent en argument supplémentaire le formatteur (argument ppf) où l'impression se produira. Cela généralise les fonctions d'impression qui peuvent maintenant imprimer sur n'importe quel formateur défini dans le programme, et permet en outre d'utiliser le format %a, celui qu'on utilise pour imprimer un argument de printf avec une fonction d'impression spécialisée qu'on a préalablement définie dans le programme (ces fonctions d'impression de l'utilisateur doivent impérativement prendre un formatteur en premier argument). Par exemple

fprintf ppf "(%a)" pr_lambda lam
permet d'imprimer l'argument lam à l'aide de la fonction pr_lambda (et l'on doit avoir pr_lambda : formatter -> lambda -> unit).

Voici la fonction d'impression des lambda-termes à l'aide des formats d'impression à la printf.

#open "format";;

let ident ppf s = fprintf ppf "%s" s;;
let kwd ppf s =  fprintf ppf "%s" s;;

let rec pr_exp0 ppf = function
  | Var s ->  ident ppf s
  | lam -> fprintf ppf "@[<1>(%a)@]" pr_lambda lam

and pr_app ppf = function
  | e ->  fprintf ppf "@[<2>%a@]" pr_other_applications e

and pr_other_applications ppf f =
  match f with
  | Apply (f, arg) -> fprintf ppf "%a@ %a" pr_app f pr_exp0 arg
  | f -> pr_exp0 ppf f

and pr_lambda ppf = function
 | Lambda (s, lam) ->
     fprintf ppf "@[<1>%a%a%a@ %a@]" kwd "\\" ident s kwd "." pr_lambda lam
 | e -> pr_app ppf e;;

let print_lambda = pr_lambda std_formatter;;
On obtient:
print_lambda (parse_lambda "(\x.x)");;
\x. x- : unit = ()


Page de présentation de Caml Dernière modification: vendredi 26 mars 2004
Copyright © 1995 - 2004, INRIA tous droits réservés.

Contacter l'auteur Pierre.Weis@inria.fr