Как работает Haskell printf?
Безопасность типов в Haskell не имеет себе равных только в языках с зависимой типизацией. Но с Text.Printf происходит какая-то глубокая магия, которая кажется довольно шаткой.
> printf "%d\n" 3
3
> printf "%s %f %d" "foo" 3.3 3
foo 3.3 3
Какая глубокая магия стоит за этим? Как можно Text.Printf.printf
функция принимает переменные аргументы, как это?
Какой общий метод используется для учета переменных аргументов в Haskell и как он работает?
(Примечание: некоторые типы безопасности, очевидно, теряются при использовании этой техники.)
> :t printf "%d\n" "foo"
printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t
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
выполняет действие ввода-вывода, которое создается рекурсивно, пока не останется больше аргументов, после чего мы просто выполняем его.
*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)