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

безопасность типа Haskell является вторым нет только для зависимо-типизированных языков. Но есть какая-то глубокая магия происходит с текст.Printf это как-то типа-шаткий.

> printf "%dn" 3
3
> printf "%s %f %d" "foo" 3.3 3
foo 3.3 3

что за глубокая магия стоит за этим? Как Text.Printf.printf функции принимают аргументы вариативная такой?

каков общий метод, используемый для учета вариативных аргументов в Haskell, и как он работает?

(Примечание: некоторая безопасность типа, по-видимому, теряется при использовании этой техники.)

> :t printf "%dn" "foo"
printf "%dn" "foo" :: (PrintfType ([Char] -> t)) => t

1 ответов


фокус в том, чтобы использовать классы типа. В случае printf, ключ PrintfType тип класса. Он не предоставляет никаких методов, но важная часть-это в любом случае.

class PrintfType r
printf :: PrintfType r => String -> r

так printf имеет перегруженный тип возвращаемого значения. В тривиальном случае у нас нет дополнительных аргументов, поэтому нам нужно иметь возможность создавать экземпляры r to IO (). Для этого у нас есть экземпляр

instance PrintfType (IO ())

далее, чтобы поддержать переменное количество аргументов, нам нужно использовать рекурсию на уровне экземпляра. В частности, нам нужен экземпляр, чтобы if r это PrintfType тип функции x -> r тоже PrintfType.

-- instance PrintfType r => PrintfType (x -> r)

конечно, мы хотим только поддерживать аргументы, которые действительно могут быть отформатированы. Вот где второй тип class 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)