Typkonstruktor-Klassen Bisher haben wir Klassen eingesetzt um Funktionen für unterschiedliche Typen zu überladen. Diese Idee lässt sich auf Typkonstruktoren fortsetzen. Wir haben zum Beispiel zwei map-Funktionen kennen gelernt. Eine für Listen: map :: (a -> b) -> [a] -> [b] map _ [] = [] map f (x:xs) = f x : map f xs und eine für Bäume: data Tree a = Empty | Node (Tree a) a (Tree a) mapTree :: (a -> b) -> Tree a -> Tree b mapTree _ Empty = Empty mapTree f (Node l x r) = Node (mapTree f l) (f x) (mapTree f r) Einstellige Typkonstruktoren, für die man eine solche map-Funktion definieren kann, heißen Funktoren: class Functor f where fmap :: (a -> b) -> f a -> f b Die Variable f ist hier eine Typkonstruktor-Variable, d.h. sie abstrahiert von einstelligen Typkonstruktoren. Wir können nun folgende Functor-Instanzen angeben: instance Functor [] where fmap = map instance Functor Tree where fmap = mapTree Auch für Maybe können wir eine Instanz angeben: instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just x) = Just (f x) Die Instanz für den Typkonstruktor Maybe wendet die gegebene Funktion auf den optionalen Wert an, wenn einer vorhanden ist. Die Klasse Functor erlaubt es Funktionen wie die folgende zu schreiben, die man sowohl auf Listen als auch auf Bäumen oder Maybe-Werten oder beliebigen anderen FunctorInstanzen anwenden kann: 1 inc :: Functor f => f Int -> f Int inc = fmap (+1) Auch IO ist ein einstelliger Typkonstruktor. Können wir für ihn auch eine FunctorInstanz angeben? Ja: instance Functor IO where fmap f a = do x <- a return (f x) Die Functor-Klasse und die gezeigten Instanzen (bis auf die für Tree) sind in Haskell vordefiniert, Sie können sie also verwenden, ohne sie selbst zu definieren. Mit der Functor-Instanz für IO können wir das folgende Programm schreiben: main = do x <- fmap length getLine print x Wenn wir es ausführen, müssen wir eine Zeile eingeben und bekommen dann deren Länge angezeigt: ghci> main abc 3 Hier ein weiteres Beispiel, das zeigt, wie man den ersten Kommandozeilen-Parameter ausgeben kann: main = do x <- fmap head getArgs print x Speichert man dieses Programm in der Datei print-first-arg.hs, kann man es wie folgt ausführen: bash# runhaskell print-first-arg.hs 42 43 44 42 Instanzen der Klasse Functor müssen die folgenden sogenannten Funktor-Gesetze erfüllen: fmap id = id fmap (f . g) = fmap f . fmap g Das heißt, fmap ist ein Homomorphismus. Wir überprüfen die Gesetze beispielhaft für die Maybe-Instanz: 2 fmap id Nothing = Nothing = id Nothing fmap id (Just x) = Just (id x) = id (Just x) fmap (f . g) Nothing = Nothing = fmap f (fmap g Nothing) = (fmap f . fmap g) Nothing fmap (f . g) (Just x) = Just ((f . g) x) = Just (f (g x)) = fmap f (fmap g (Just x)) = (fmap f . fmap g) (Just x) Für Listen und Bäume zeigt man die Gesetze per struktureller Induktion. Um zu zeigen, dass die IO-Instanz die Funktor-Gesetze erfüllt, benötigt man Gesetze für die doNotation, die wir bisher noch nicht kennen. 3