Comment fonctionne Haskell printf?

La sécurité de type Haskell est sans égal pour les langages à typage dépendant. Mais il y a de la magie profonde avec Text.Printf qui semble plutôt génial .

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

Quelle est la magie profonde derrière cela? Comment la fonction Text.Printf.printf peut- Text.Printf.printf utiliser des arguments variadiques comme celui-ci?

Quelle est la technique générale utilisée pour autoriser les arguments variadiques dans Haskell et comment fonctionne-t-il?

(Note de côté: une certaine sécurité de type est apparemment perdue lors de l’utilisation de cette technique.)

 > :t printf "%d\n" "foo" printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t 

L’astuce consiste à utiliser des classes de type. Dans le cas de printf , la clé est la classe de type PrintfType . Il n’expose aucune méthode, mais la partie importante est dans les types de toute façon.

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

Donc, printf a un type de retour surchargé. Dans le cas sortingvial, nous n’avons pas d’arguments supplémentaires, nous devons donc pouvoir instancier r sur 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 r est un PrintfType , un type de fonction x -> r soit également un type PrintfType .

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

Bien entendu, nous ne souhaitons que prendre en charge les arguments pouvant être formatés. C’est là que la deuxième classe de type PrintfArg entre en jeu. Donc 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 classe Show et les imprime 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 sx = bar (s >> print x) 

Ici, bar prend une action IO qui est construite de manière récursive 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 classe Testable a une instance pour le cas Bool base, et une autre récursive pour les fonctions qui prennent des arguments dans la classe Arbitrary .

 class Testable a instance Testable Bool instance (Arbitrary x, Testable r) => Testable (x -> r)