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

104

Безопасность типов 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
Дэн Бертон
источник
15
Вы можете получить типобезопасный printf только с использованием зависимых типов.
августа
9
Леннарт совершенно прав. Типовая безопасность Haskell уступает языкам с даже более зависимыми типами, чем Haskell. Конечно, вы можете сделать тип вещи, подобный printf, безопасным, если выберете для формата более информативный тип, чем String.
Свинарник
3
см. oleg для нескольких вариантов printf: okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv
1
@augustss Вы можете получить типобезопасный printf, только используя зависимые типы ИЛИ ШАБЛОН HASKELL! ;-)
MathematicalOrchid
3
@Mat MathematicalOrchid Template Haskell не в счет. :)
август 05

Ответы:

131

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

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

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

instance PrintfType (IO ())

Затем, чтобы поддерживать переменное количество аргументов, нам нужно использовать рекурсию на уровне экземпляра. В частности, нам нужен экземпляр, чтобы if ris a PrintfType, тип функции x -> rтакже был a 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) 
хаммар
источник
Отличный ответ. Я просто хотел указать, что haskell определяет тип Foo на основе применяемых аргументов. Чтобы понять это, вы можете указать тип эксплицитности Foo следующим образом: λ> (foo :: (Show x, Show y) => x -> y -> IO ()) 3 "hello"
redfish64
1
Хотя я понимаю, как реализована часть аргументов переменной длины, я все еще не понимаю, как компилятор отклоняет printf "%d" True. Для меня это очень мистично, так как кажется, что значение времени выполнения (?) "%d"Расшифровывается во время компиляции и требует наличия Int. Для меня это совершенно сбивает с толку. . . особенно потому, что в исходном коде не используются такие вещи, как DataKindsили TemplateHaskell(я проверил исходный код, но не понял его.)
Томас
2
@ThomasEding Причина, по которой компилятор отклоняет, printf "%d" Trueсостоит в том, что нет Boolэкземпляра PrintfArg. Если вы передаете аргумент неправильного типа, у которого есть экземпляр PrintfArg, он компилируется и генерирует исключение во время выполнения. Пример:printf "%d" "hi"
Трэвис Сандерленд