Как работает Haskell printf? - PullRequest
       28

Как работает Haskell printf?

96 голосов
/ 20 октября 2011

Безопасность типов в 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 Ответ

125 голосов
/ 20 октября 2011

Хитрость заключается в использовании классов типов.В случае 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) 
...