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

Безопасность типа Haskell - это второй none только для языков с надписью. Но есть некоторая глубокая магия, происходящая с 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 принимает 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)