Хитрість полягає у використанні класів типів. У випадку з printfключовим є PrintfTypeклас типу. Він не викриває жодних методів, але важлива частина все-таки є у типах.
class PrintfType r
printf :: PrintfType r => String -> r
Таким чином, printfмає перевантажений тип повернення. У тривіальному випадку, у нас немає ніяких додаткових аргументів, тому ми повинні бути в змозі створити екземпляр rв IO (). Для цього у нас є екземпляр
instance PrintfType (IO ())
Далі, щоб підтримувати змінну кількість аргументів, нам потрібно використовувати рекурсію на рівні примірника. Зокрема, нам потрібен екземпляр, щоб, якщо rє a PrintfType, тип функції x -> rтакож є a PrintfType.
-- instance PrintfType r => PrintfType (x -> r)
Звичайно, ми хочемо лише підтримати аргументи, які насправді можуть бути відформатовані. Тут PrintfArgнадходить клас другого типу . Отже, власне екземпляр є
instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)
Ось спрощена версія, яка бере будь-яку кількість аргументів у Showкласі та просто роздруковує їх:
{-# 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)
Тут barробиться дія вводу-виводу, яка створюється рекурсивно, поки не буде більше аргументів, і тоді ми просто виконуємо її.
*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True
QuickCheck також використовує ту саму методику, коли Testableклас має екземпляр для базового випадку Bool, а рекурсивний - для функцій, які беруть аргументи в Arbitraryкласі.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)