L'astuce consiste à utiliser des classes de types. Dans le cas de printf
, la clé est la PrintfType
classe 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 printf
un type de retour surchargé. Dans le cas trivial, nous n'avons pas d'arguments supplémentaires, nous devons donc pouvoir instancier r
vers 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 a PrintfType
, un type de fonction x -> r
est é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 Show
classe 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, bar
prend 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 Testable
classe a une instance pour le cas de base Bool
et une instance récursive pour les fonctions qui prennent des arguments dans la Arbitrary
classe.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)