web-dev-qa-db-fra.com

Comment fonctionne Haskell printf?

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
96
Dan Burton

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) 
125
hammar