Хитрость заключается в использовании классов типов.В случае printf
ключ является классом типа PrintfType
.Он не предоставляет никаких методов, но в любом случае важная часть находится в типах.
class PrintfType r
printf :: PrintfType r => String -> r
Так что printf
имеет перегруженный тип возврата.В тривиальном случае у нас нет дополнительных аргументов, поэтому мы должны иметь возможность создавать экземпляры от r
до IO ()
.Для этого у нас есть экземпляр
instance PrintfType (IO ())
Далее, чтобы поддерживать переменное количество аргументов, нам нужно использовать рекурсию на уровне экземпляра.В частности, нам нужен экземпляр, так что если r
является PrintfType
, тип функции x -> r
также является 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)