Der Trick besteht darin, Typklassen zu verwenden. Im Fall von printf
ist der Schlüssel die PrintfType
Typklasse. Es werden keine Methoden verfügbar gemacht, aber der wichtige Teil liegt trotzdem in den Typen.
class PrintfType r
printf :: PrintfType r => String -> r
Hat printf
also einen überladenen Rückgabetyp. Im trivialen Fall haben wir keine zusätzlichen Argumente, so dass wir zu instanziiert müssen in die Lage r
zu IO ()
. Dafür haben wir die Instanz
instance PrintfType (IO ())
Um eine variable Anzahl von Argumenten zu unterstützen, müssen wir als nächstes die Rekursion auf Instanzebene verwenden. Insbesondere benötigen wir eine Instanz, damit, wenn a r
ist PrintfType
, ein Funktionstyp x -> r
auch a ist PrintfType
.
-- instance PrintfType r => PrintfType (x -> r)
Natürlich wollen wir nur Argumente unterstützen, die tatsächlich formatiert werden können. Hier kommt die zweite Typklasse ins PrintfArg
Spiel. Die eigentliche Instanz ist also
instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)
Hier ist eine vereinfachte Version, die eine beliebige Anzahl von Argumenten in der Show
Klasse verwendet und diese nur druckt:
{-# 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)
Hier wird bar
eine E / A-Aktion ausgeführt, die rekursiv aufgebaut wird, bis keine Argumente mehr vorhanden sind. An diesem Punkt führen wir sie einfach aus.
*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True
QuickCheck verwendet dieselbe Technik, bei der die Testable
Klasse eine Instanz für den Basisfall Bool
und eine rekursive Instanz für Funktionen hat, die Argumente in der Arbitrary
Klasse annehmen .
class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r)