O truque é usar classes de tipo. No caso de printf
, a chave é a PrintfType
classe 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, printf
tem um tipo de retorno sobrecarregado. No caso trivial, não temos argumentos extras, então precisamos ser capazes de instanciar r
para 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 r
for a PrintfType
, um tipo de função x -> r
também seja a PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Claro, queremos apenas suportar argumentos que possam realmente ser formatados. É aí que PrintfArg
entra 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 Show
classe 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, bar
executa 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 Testable
classe tem uma instância para o caso base Bool
e uma recursiva para funções que recebem argumentos da Arbitrary
classe.
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)