Ответ 1
Хитрость заключается в использовании классов типов. В случае 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
принимает IO-действие, которое создается рекурсивно, пока не будет больше аргументов, после чего мы просто выполним его.
*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)