Sztuczka polega na użyciu klas typów. W przypadku printf
klucza jest to PrintfType
klasa typu. Nie ujawnia żadnych metod, ale ważna część i tak jest w typach.
class PrintfType r
printf :: PrintfType r => String -> r
Więc printf
ma przeciążony typ powrotu. W trywialnym przypadku nie mamy żadnych dodatkowych argumentów, więc musimy mieć możliwość wystąpienia r
do IO ()
. W tym celu mamy instancję
instance PrintfType (IO ())
Następnie, aby obsłużyć zmienną liczbę argumentów, musimy użyć rekursji na poziomie instancji. W szczególności potrzebujemy instancji, więc jeśli r
jest a PrintfType
, typ funkcji x -> r
również jest PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Oczywiście chcemy obsługiwać tylko argumenty, które faktycznie można sformatować. W tym miejscu PrintfArg
pojawia się klasa drugiego typu . A więc rzeczywista instancja jest
instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)
Oto uproszczona wersja, która pobiera dowolną liczbę argumentów z Show
klasy i po prostu je drukuje:
{-# 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)
Tutaj bar
wykonuje akcję IO, która jest budowana rekurencyjnie, dopóki nie ma więcej argumentów, w którym to momencie po prostu ją wykonujemy.
*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True
QuickCheck używa również tej samej techniki, w której Testable
klasa ma instancję dla przypadku podstawowego Bool
i rekursywną dla funkcji, które pobierają argumenty w Arbitrary
klasie.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)