Como funciona o Haskell printf?

104

A segurança de tipos de Haskell é incomparável apenas em relação às linguagens com tipos dependentes. Mas existe uma magia profunda acontecendo com Text.Printf que parece um tanto quanto duvidosa .

> printf "%d\n" 3
3
> printf "%s %f %d" "foo" 3.3 3
foo 3.3 3

Qual é a magia profunda por trás disso? Como a Text.Printf.printffunção pode aceitar argumentos variados como este?

Qual é a técnica geral usada para permitir argumentos variáveis ​​em Haskell e como isso funciona?

(Nota lateral: algum tipo de segurança é aparentemente perdido ao usar esta técnica.)

> :t printf "%d\n" "foo"
printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t
Dan Burton
fonte
15
Você só pode obter um printf de tipo seguro usando tipos dependentes.
agosto
9
Lennart está certo. A segurança de tipo de Haskell é secundária para as linguagens com tipos ainda mais dependentes do que Haskell. Claro, você pode tornar um tipo de coisa semelhante a printf seguro se escolher um tipo mais informativo do que String para o formato.
pigworker
3
veja oleg para múltiplas variantes de printf: okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv
1
@augustss Você só pode obter um printf de tipo seguro usando tipos dependentes OU TEMPLATE HASKELL! ;-)
MathematicalOrchid
3
@MathematicalOrchid Template Haskell não conta. :)
agosto

Respostas:

131

O truque é usar classes de tipo. No caso de printf, a chave é a PrintfTypeclasse de tipo. Ele não expõe nenhum método, mas de qualquer maneira a parte importante está nos tipos.

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

Então, printftem um tipo de retorno sobrecarregado. No caso trivial, não temos argumentos extras, então precisamos ser capazes de instanciar rpara IO (). Para isso, temos a instância

instance PrintfType (IO ())

Em seguida, para suportar um número variável de argumentos, precisamos usar a recursão no nível da instância. Em particular, precisamos de uma instância para que se rfor a PrintfType, um tipo de função x -> rtambém seja a PrintfType.

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

Claro, queremos apenas suportar argumentos que possam realmente ser formatados. É aí que PrintfArgentra a segunda classe de tipo . Portanto, a instância real é

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

Esta é uma versão simplificada que pega qualquer número de argumentos na Showclasse e apenas os imprime:

{-# 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)

Aqui, barexecuta uma ação IO que é construída recursivamente até que não haja mais argumentos, momento em que simplesmente a executamos.

*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True

QuickCheck também usa a mesma técnica, onde a Testableclasse tem uma instância para o caso base Boole uma recursiva para funções que recebem argumentos da Arbitraryclasse.

class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r) 
Hammar
fonte
Ótima resposta. Eu só queria salientar que haskell é descobrir o tipo de Foo com base nos argumentos aplicados. Para entender isso, você pode querer especificar o tipo de Foo explicitamente da seguinte forma: λ> (foo :: (Mostrar x, Mostrar y) => x -> y -> IO ()) 3 "hello"
redfish64
1
Embora eu entenda como a parte do argumento de comprimento variável é implementada, ainda não entendo como o compilador rejeita printf "%d" True. Isso é muito místico para mim, pois parece que o valor do tempo de execução (?) É "%d"decifrado em tempo de compilação para exigir um Int. Isso é absolutamente desconcertante para mim. . . especialmente porque o código-fonte não usa coisas como DataKindsou TemplateHaskell(eu verifiquei o código-fonte, mas não o compreendi.)
Thomas Eding
2
@ThomasEding O motivo pelo qual o compilador rejeita printf "%d" Trueé porque não há Boolinstância de PrintfArg. Se você passar um argumento do tipo errado de que não tem uma instância PrintfArg, ele faz de compilação e lança uma exceção em tempo de execução. Ex:printf "%d" "hi"
Travis Sunderland