Il trucco sta nell'usare classi di tipo. Nel caso di printf
, la chiave è la PrintfType
classe del tipo. Non espone alcun metodo, ma la parte importante è comunque nei tipi.
class PrintfType r
printf :: PrintfType r => String -> r
Quindi printf
ha un tipo di ritorno sovraccarico. Nel caso banale, non abbiamo argomenti extra, quindi dobbiamo essere in grado di istanziare r
in IO ()
. Per questo, abbiamo l'istanza
instance PrintfType (IO ())
Successivamente, per supportare un numero variabile di argomenti, è necessario utilizzare la ricorsione a livello di istanza. In particolare, abbiamo bisogno di un'istanza in modo che se r
è a PrintfType
, anche un tipo di funzione x -> r
è a PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Naturalmente, vogliamo supportare solo argomenti che possono essere effettivamente formattati. È qui che PrintfArg
entra in gioco la seconda classe di tipo . Quindi l'istanza reale è
instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)
Ecco una versione semplificata che accetta un numero qualsiasi di argomenti nella Show
classe e li stampa semplicemente:
{-# 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)
Qui, bar
esegue un'azione IO che viene costruita in modo ricorsivo fino a quando non ci sono più argomenti, a quel punto la eseguiamo semplicemente.
*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True
QuickCheck utilizza anche la stessa tecnica, dove la Testable
classe ha un'istanza per il caso base Bool
e una ricorsiva per le funzioni che accettano argomenti nella Arbitrary
classe.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)