¿Cómo funciona Haskell printf?

104

La seguridad de tipos de Haskell es insuperable solo para los lenguajes de escritura dependiente. Pero hay algo de magia profunda sucediendo con Text.Printf que parece bastante inestable .

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

¿Cuál es la magia profunda detrás de esto? ¿Cómo puede elText.Printf.printf función tomar argumentos variados como este?

¿Cuál es la técnica general utilizada para permitir argumentos variados en Haskell y cómo funciona?

(Nota al margen: aparentemente se pierde algún tipo de seguridad cuando se usa esta técnica).

> :t printf "%d\n" "foo"
printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t
Dan Burton
fuente
15
Solo puede obtener una impresión segura de tipos utilizando tipos dependientes.
agosto de 2011
9
Lennart tiene razón. La seguridad de tipos de Haskell es superada por los idiomas con tipos aún más dependientes que Haskell. Por supuesto, puede hacer que un tipo de cosa similar a printf sea seguro si elige un tipo más informativo que String para el formato.
trabajador porcino
3
ver oleg para múltiples variantes de printf: okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv
1
@augustss ¡Solo puede obtener un printf de tipo seguro utilizando tipos dependientes O PLANTILLA HASKELL! ;-)
MathematicalOrchid
3
@MathematicalOrchid Template Haskell no cuenta. :)
agosto

Respuestas:

131

El truco consiste en utilizar clases de tipos. En el caso de printf, la clave es la PrintfTypeclase de tipo. No expone ningún método, pero la parte importante está en los tipos de todos modos.

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

También printftiene un tipo de retorno sobrecargado. En el caso trivial, no tenemos argumentos adicionales, por lo que debemos poder crear runa instancia de IO (). Para esto, tenemos la instancia

instance PrintfType (IO ())

A continuación, para admitir un número variable de argumentos, debemos utilizar la recursividad a nivel de instancia. En particular, necesitamos una instancia para que si res a PrintfType, un tipo de función x -> rtambién sea a PrintfType.

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

Por supuesto, solo queremos admitir argumentos que realmente se puedan formatear. Ahí es donde PrintfArgentra en juego la segunda clase de tipos . Así que la instancia real es

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

Aquí hay una versión simplificada que toma cualquier número de argumentos en la Showclase y simplemente los 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)

Aquí, bartoma una acción IO que se construye de forma recursiva hasta que no hay más argumentos, momento en el que simplemente la ejecutamos.

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

QuickCheck también usa la misma técnica, donde la Testableclase tiene una instancia para el caso base Booly una recursiva para funciones que toman argumentos en la Arbitraryclase.

class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r) 
Hammar
fuente
Gran respuesta. Solo quería señalar que Haskell está averiguando el tipo de Foo basándose en los argumentos aplicados. Para entender esto, es posible que desee especificar el tipo de Foo explícitamente de la siguiente manera: λ> (foo :: (Mostrar x, Mostrar y) => x -> y -> IO ()) 3 "hola"
redfish64
1
Si bien entiendo cómo se implementa la parte del argumento de longitud variable, todavía no entiendo cómo rechaza el compilador printf "%d" True. Esto es muy místico para mí, ya que parece que el valor de tiempo de ejecución (?) Se "%d"descifra en tiempo de compilación para requerir un Int. Esto es absolutamente desconcertante para mí. . . especialmente porque el código fuente no usa cosas como DataKindso TemplateHaskell(revisé el código fuente, pero no lo
entendí
2
@ThomasEding La razón por la que el compilador rechaza printf "%d" Truees porque no hay una Boolinstancia de PrintfArg. Si pasa un argumento del tipo equivocado de que no tiene una instancia de PrintfArg, lo hace de compilación y se produce una excepción en tiempo de ejecución. Ej .:printf "%d" "hi"
Travis Sunderland