¿Cómo funciona Haskell printf?
variadic-functions polyvariadic (1)
El truco es usar clases de tipo. En el caso de printf
, la clave es la clase de tipo PrintfType
. 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
Entonces printf
tiene un tipo de devolución sobrecargado. En el caso trivial, no tenemos argumentos adicionales, por lo que debemos poder instanciar r
a IO ()
. Para esto, tenemos la instancia
instance PrintfType (IO ())
A continuación, para admitir un número variable de argumentos, necesitamos usar la recursión en el nivel de instancia. En particular, necesitamos una instancia para que si r
es un PrintfType
, una función tipo x -> r
también sea PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Por supuesto, solo queremos admitir argumentos que en realidad pueden formatearse. Ahí es donde entra la segunda clase tipo PrintfArg
. Entonces, la instancia real es
instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)
Aquí hay una versión simplificada que toma cualquier cantidad de argumentos en la clase Show
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í, bar
toma una acción IO que se construye recursivamente hasta que no haya más argumentos, en cuyo punto simplemente lo 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 clase Testable
tiene una instancia para el caso base Bool
, y una recursiva para funciones que toman argumentos en la clase Arbitrary
.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)
La seguridad de tipo de Haskell es insuperable solo para los idiomas de tipo dependiente. Pero hay algo de magia profunda pasando con Text.Printf que parece algo poco Text.Printf .
> 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 la función Text.Printf.printf 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: al parecer, se pierde algún tipo de seguridad al usar esta técnica).
> :t printf "%d/n" "foo"
printf "%d/n" "foo" :: (PrintfType ([Char] -> t)) => t