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