Le module Printf contient des fonctions assez particulières. Ces fonctions utilisent des formats qui sont vérifiés à la compilation. Le typage de ces fonctions dépend du format donné.

Ainsi : printf "%s %d" est une fonction de type string -> int -> unit. C'est une fonction à part entière, l'application partielle est bien sûr disponible. Dans l'exemple ci-dessus, "%s %d" n'est donc pas une chaine de caractère, mais un format.

Faux printf

On souhaite parfois avoir un "faux printf", c'est-à-dire une fonction qui possède le même type que printf, mais qui n'affiche rien.

Il existe plusieurs façons de faire cela. Par exemple, on peut appeler kfprintf (une variante de printf qui utilise une fonction "finale", transformant la sortie) avec "ignore" en argument.

let fake_printf fmt = Printf.kfprintf ignore stdout fmt

Ou encore, créer un flux vide et le donner en argument à fprintf :

let nullOut = new StreamWriter(Stream.Null) :> TextWriter
let fake_printf fmt = fprintf nullOut fmt

Debug

La fonction précédente peut s'utiliser avec une directive de compilation, du type :

#if DEBUG
let debug = printfn
#else
let debug = fake_printf
#endif 

Pour plus de flexibilité, il est intéressant de définir des niveaux de verbosité dans le debug, voire de modifier cette verbosité à l'exécution. Un exemple tout simple :

let verbose = ref 3
let debug n =
    if n >= !verbose then printfn
                     else fake_printf
 
 
> debug 2 "test %d" 42;;
val it : unit = ()
> debug 3 "test %d" 42;;
test 42
val it : unit = ()

Choisir le format à l'exécution

Pour des raisons de typage, le format doit être connu à la compilation. Le code suivant est rejeté par le compilateur, puisque s a un type string et non format :

let s = "%s%d"
printf s "test" 42

Il suffit alors d'ajouter une annotation de type, pour forcer le type de s :

let s: Text.Format<_,_,_,_> = "%s%d"
printf s "test" 42

Ce code fonctionne, mais l'annotation de type est un peu verbeuse : le type Format est en effet paramétré par 4 types pour des raisons d'implémentation. Si on souhaite manipuler régulièrement des formats, je propose d'utiliser un opérateur préfixe pour ajouter la contrainte.

let (!$) (fmt: Text.Format<_,_,_,_>) = fmt

Ainsi :

let s = ref !$"%s%d"          // définit un format mutable
//s := "fooooo %f"            // le compilateur refuse cette ligne !
s := "String = %s ; int = %d" // cette ligne est acceptée
printf !s "test" 42           // affiche "String = test ; int = 42" 

Printf multiple

Les exemples précédents reposaient sur l'application partielle pour récupérer tous les arguments de printf. Cependant, cela ne fonctionne pour les cas plus complexes. Par exemple, le code suivant manque de généricité :

let dup_printf fmt =
  printfn fmt
  printfn fmt

dup_printf n'accepte que les formats qui n'impliquent pas d'argument supplémentaire. "test" sera accepté mais "test %s" sera rejeté par le compilateur.

La solution la plus simple est d'utiliser la surcharge.

type Printf(streams) =
  [<OverloadID("0arg")>]
  member p.Dup fmt = List.iter (fun s -> fprintfn s fmt) streams
  [<OverloadID("1arg")>]
  member p.Dup fmt x = List.iter (fun s -> fprintfn s fmt x) streams
  [<OverloadID("2args")>]
  member p.Dup fmt x y = List.iter (fun s -> fprintfn s fmt x y) streams
  ...

Le type Printf se construit à partir d'une liste de flux de sortie et écrit la sortie de fprintf dans chacun des flux.

  let pr = Printf [stdout; stderr]

Pour utiliser Dup, il est nécessaire d'indiquer explicitement que c'est un format (est-ce une limitation de l'inférence ? Est-ce un bug ?). Utilisons donc l'opérateur défini auparavant :

  pr.Dup !$"test"

Ce qui affiche "test" sur la sortie standard et sur la sortie d'erreur. Une utilisation plus intelligente pourrait être d'afficher le message à l'écran et de le logguer dans un fichier.

Unsafe printf

Si l'on souhaite construire un format à l'exécution, laisser l'utilisateur le choisir ou pour d'autres raisons, il est possible de passer outre le système de typage. C'est évidemment dangereux.

  printf (Printf.TextWriterFormat ("test" + "%s")) "foo"

En cas d'erreur dans le format, une exception est lancée à l'exécution.

Pour aller plus loin

Il est aussi possible d'encapsuler la fonction dans un objet pour obtenir une certaine souplesse. Don Syme a donné quelques exemples très intéressants : http://cs.hubfs.net/forums/thread/6194.aspx