English version
Accueil     À propos     Téléchargement     Ressources     Contactez-nous    

Utiliser le module Format

Le module Format des librairies standard de Caml Light et OCaml 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 »).

Principes

La coupure des lignes repose sur trois concepts:

Les 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érale, 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 :

(---
 (----
  (---
  )
 )
)

Conseils pratiques

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. 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.
  4. N'essayez pas de forcer l'espacement à l'aide de blancs explicites dans les chaînes de caractères à imprimer. Pour chaque espace nécessaire, utilisez une indication de coupure (print_space ()), à moins que vous ne vouliez pas que la ligne soit coupée à cet endroit. Par exemple, imaginez que vous vouliez imprimer une définition Caml, disons let rec ident = expression. Vous allez probablement considérer les 3 premiers blancs comme des « blancs insécables » et les inclure directement dans une chaîne de caractères, et écrire la chaîne "let  rec  " avant l'identificateur et la chaîne   = après lui; en revanche, l'espace qui suit le caractère = doit être une indication de coupure, puisqu'il est d'usage (et élégant) de couper la ligne à cet endroit pour indenter la partie expression d'une définition. En conclusion, il est bien sûr souvent 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.)
  5. Ne forcez jamais de coupures de ligne, laissez le moteur d'impression le faire pour vous: c'est son travail! En particulier, n'utilisez pas la procédure force_newline: son usage provoque bien une coupure de ligne, mais il provoque aussi une réinitialisation partielle du moteur d'impression qui déséquilibre tout le reste de l'impression.
  6. 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!
  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.)

Impression sur la sortie standard: utilisation de 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 concret

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 :

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

J'utilise le module format pour imprimer les lambda-termes:

open Format;;

let ident = print_string;;
let kwd = print_string;;
val ident : string -> unit = <fun>
val kwd : string -> unit = <fun>

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;;
val print_app : lambda -> unit = <fun>
val print_other_applications : lambda -> unit = <fun>
val print_lambda : lambda -> unit = <fun>

En Caml Light, remplacez la première ligne par :

#open "format";;

Impression la plus générale: utilisation de fprintf

On utilise maintenant la fonction fprintf et toutes les fonctions d'impression prennent en argument supplémentaire le formatteur (c'est l'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 cela permet en outre d'utiliser la conversion %a, celle qu'on utilise pour imprimer un argument de fprintf 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 prennent aussi un formatteur en premier argument).

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

open Format;;

let ident ppf s = fprintf ppf "%s" s;;
let kwd ppf s = fprintf ppf "%s" s;;
val ident : Format.formatter -> string -> unit
val kwd : Format.formatter -> string -> unit

let rec pr_exp0 ppf = function
  | Var s -> fprintf ppf "%a" ident 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
;;
val pr_app : Format.formatter -> lambda -> unit
val pr_other_applications : Format.formatter -> lambda -> unit
val pr_lambda : Format.formatter -> lambda -> unit

Armés de ces fonctions d'impression générales, les procédures d'impression sur la sortie standard ou la sortie d'erreur s'obtiennent facilement par application partielle:.

let print_lambda = pr_lambda std_formatter;;
let eprint_lambda = pr_lambda err_formatter;;
val print_lambda : lambda -> unit
val eprint_lambda : lambda -> unit