La sécurité de type de Haskell est la deuxième à aucun uniquement aux langues typées de manière dépendante. Mais il y a un peu de magie profonde avec Text.Printf qui semble plutôt typé.
> 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 le Text.Printf.printf
la fonction prend des arguments variadiques comme celui-ci?
Quelle est la technique générale utilisée pour autoriser les arguments variadiques dans Haskell, et comment ça marche?
(Remarque: 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 types. Dans le cas de printf
, la clé est la classe de type PrintfType
. Il n'expose aucune méthode, mais la partie importante est quand même dans les types.
class PrintfType r
printf :: PrintfType r => String -> r
Donc printf
a un type de retour surchargé. Dans le cas trivial, nous n'avons pas d'arguments supplémentaires, nous devons donc pouvoir instancier r
en IO ()
. Pour cela, nous avons l'instance
instance PrintfType (IO ())
Ensuite, afin de 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
Est également un PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Bien sûr, nous ne voulons prendre en charge que des arguments qui peuvent effectivement ê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 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 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 de base Bool
et une instance 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)