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)