Sztuczka polega na użyciu klas typów. W przypadku printfklucza jest to PrintfTypeklasa typu. Nie ujawnia żadnych metod, ale ważna część i tak jest w typach.
class PrintfType r
printf :: PrintfType r => String -> r
Więc printfma przeciążony typ powrotu. W trywialnym przypadku nie mamy żadnych dodatkowych argumentów, więc musimy mieć możliwość wystąpienia rdo 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 rjest a PrintfType, typ funkcji x -> rró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 PrintfArgpojawia 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 Showklasy 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 barwykonuje 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 Testableklasa ma instancję dla przypadku podstawowego Booli rekursywną dla funkcji, które pobierają argumenty w Arbitraryklasie.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)