Хитрість полягає у використанні класів типів. У випадку з 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)