Comment fonctionne Haskell printf?

104

La sécurité des types de Haskell est inégalée uniquement par rapport aux langages à typage dépendant. Mais il y a une magie profonde en cours avec Text.Printf qui semble plutôt insensée .

> printf "%d\n" 3
3
> printf "%s %f %d" "foo" 3.3 3
foo 3.3 3

Quelle est la magie profonde derrière tout cela? Comment la Text.Printf.printffonction peut-elle accepter des arguments variadiques comme celui-ci?

Quelle est la technique générale utilisée pour permettre des arguments variadiques dans Haskell, et comment cela fonctionne-t-il?

(Note latérale: certains types de sécurité sont apparemment perdus lors de l'utilisation de cette technique.)

> :t printf "%d\n" "foo"
printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t
Dan Burton
la source
15
Vous ne pouvez obtenir une impression sécurisée de type qu'en utilisant des types dépendants.
2011
9
Lennart a tout à fait raison. La sécurité des types de Haskell est la seconde après les langages avec des types encore plus dépendants que Haskell. Bien sûr, vous pouvez sécuriser un type d'objet de type printf si vous choisissez un type plus informatif que String pour le format.
pigworker
3
voir oleg pour plusieurs variantes de printf: okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv
1
@augustss Vous ne pouvez obtenir un printf de type sécurisé qu'en utilisant des types dépendants OU TEMPLATE HASKELL! ;-)
MathematicalOrchid
3
Le modèle @MathematicalOrchid Haskell ne compte pas. :)
août

Réponses:

131

L'astuce consiste à utiliser des classes de types. Dans le cas de printf, la clé est la PrintfTypeclasse de type. Il n'expose aucune méthode, mais l'essentiel est quand même dans les types.

class PrintfType r
printf :: PrintfType r => String -> r

A donc printfun type de retour surchargé. Dans le cas trivial, nous n'avons pas d'arguments supplémentaires, nous devons donc pouvoir instancier rvers IO (). Pour cela, nous avons l'instance

instance PrintfType (IO ())

Ensuite, pour prendre en charge un nombre variable d'arguments, nous devons utiliser la récursivité au niveau de l'instance. En particulier, nous avons besoin d'une instance pour que si rest a PrintfType, un type de fonction x -> rest également a PrintfType.

-- instance PrintfType r => PrintfType (x -> r)

Bien sûr, nous voulons uniquement prendre en charge les arguments qui peuvent réellement être formatés. C'est là qu'intervient la deuxième classe de type PrintfArg. Ainsi, l'instance réelle est

instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)

Voici une version simplifiée qui prend n'importe quel nombre d'arguments dans la Showclasse et les affiche simplement:

{-# LANGUAGE FlexibleInstances #-}

foo :: FooType a => a
foo = bar (return ())

class FooType a where
    bar :: IO () -> a

instance FooType (IO ()) where
    bar = id

instance (Show x, FooType r) => FooType (x -> r) where
    bar s x = bar (s >> print x)

Ici, barprend une action IO qui est construite récursivement jusqu'à ce qu'il n'y ait plus d'arguments, à quel point nous l'exécutons simplement.

*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True

QuickCheck utilise également la même technique, où la Testableclasse a une instance pour le cas de base Boolet une instance récursive pour les fonctions qui prennent des arguments dans la Arbitraryclasse.

class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r) 
Hammar
la source
Très bonne réponse. Je voulais juste souligner que haskell détermine le type de Foo en fonction des arguments appliqués. Pour comprendre cela, vous pouvez spécifier le type d'explicité de Foo comme suit: λ> (foo :: (Show x, Show y) => x -> y -> IO ()) 3 "hello"
redfish64
1
Bien que je comprenne comment la partie de l'argument de longueur variable est implémentée, je ne comprends toujours pas comment le compilateur rejette printf "%d" True. C'est très mystique pour moi, car il semble que la valeur d'exécution (?) "%d"Soit déchiffrée au moment de la compilation pour exiger un fichier Int. Cela me déroute absolument. . . d'autant plus que le code source n'utilise pas des choses comme DataKindsou TemplateHaskell(j'ai vérifié le code source, mais je ne l'ai pas compris.)
Thomas Eding
2
@ThomasEding La raison pour laquelle le compilateur rejette printf "%d" Trueest qu'il n'y a pas d' Boolinstance de PrintfArg. Si vous passez un argument de type incorrect qui ne possède une instance de PrintfArg, il ne compile et lance une exception à l' exécution. Ex:printf "%d" "hi"
Travis Sunderland