Printf avancé
By Laurent Le Brun on Wednesday, August 6 2008, 18:42 - [FR] Cours F# - Permalink
Des exemples avancés pour utiliser les fonctions de type printf. Il existe un certain nombre d'astuces, car cette fonction possède un type assez particulier.
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.
[fsharp] let fake_printf fmt = Printf.kfprintf ignore stdout fmt
Ou encore, créer un flux vide et le donner en argument à fprintf :
[fsharp] 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 :
[fsharp] #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 :
[fsharp] 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 :
[fsharp] let s = "%s%d" printf s "test" 42
Il suffit alors d'ajouter une annotation de type, pour forcer le type de s :
[fsharp] 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.
[fsharp] let (!$) (fmt: Text.Format<_,_,_,_>) = fmt
Ainsi :
[fsharp] 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é :
[fsharp] 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.
[fsharp] 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.
[fsharp] 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 :
[fsharp] 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.
[fsharp] 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
Comments
Merci pour cette série d'articles sur les rudiments de F#. Instructif.
Reste à savoir, le plus objectivement possible (ce qui est toujours très difficile), qu'est ce qu'apporte F# par rapport à des langages déjà très évolués comme C#.
En tout cas merci encore, ça aide à se faire une idée (très mitigée pour l'instant :-) ! )