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)