El truco consiste en utilizar clases de tipos. En el caso de printf
, la clave es la PrintfType
clase 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 printf
tiene un tipo de retorno sobrecargado. En el caso trivial, no tenemos argumentos adicionales, por lo que debemos poder crear r
una 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 r
es a PrintfType
, un tipo de función x -> r
también sea a PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Por supuesto, solo queremos admitir argumentos que realmente se puedan formatear. Ahí es donde PrintfArg
entra 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 Show
clase 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 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 Testable
clase tiene una instancia para el caso base Bool
y una recursiva para funciones que toman argumentos en la Arbitrary
clase.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)