Как работает 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) 
Другие вопросы по тегам