Die Typkonstruktorklasse Monad Nachdem wir mit Functor eine erste Typkonstruktorklasse kennengelernt haben, stellt sich die Frage, ob es noch weitere sinnvolle Typkonstruktorklassen gibt. Hierzu untersuchen wir zunächst die IO-Monade und ihre Verknüpfungen genauer und erkennen die Gültigkeit folgender Gesetze: return () >> m = m m >> return () = m m >> (n >> o) = (m >> n) >> o Es fällt auf, dass (>>) assoziativ ist und return () ein neutrales Element ist, d.h. beide ergeben zusammen ein Monoid. Entsprechend gelten für return und >>= folgende Gesetze: return v >>= \x -> m = m[x/v] m >>= \x -> return x = m m >>= \x -> (n >>= \y -> o) = (m >>= \x -> n) >>= \y -> o Hierbei müssen wir beim letzten Gesetz noch zusätzlich einschränken, dass x nicht frei in o vorkommen darf1 . Diese Struktur heißt Monade. Der Monaden-Begriff stammt aus der Kategorientheorie, wo er allgemeiner von einem Funktor und zwei natürlichen Transformationen spricht und wo die Monadengesetze mathematisch formuliert sind2 . Das Wort Monade wurde von Leibniz’ entlehnt3 . In Haskell ist eine Monade also ein einstelliger Typkonstruktor m mit den Operationen return, (>>=) (und fail), welche die obigen Eigenschaften erfüllen und in der Klasse Monad zusammengefasst werden: class Monad m where (>>=) : : m a -> (a -> m b) -> m b return : : a -> m a fail : : String -> m a fails = error s (>>) : : m a -> m b -> m b p >> q = p >>= \_ -> q 1 Das freie Vorkommen einer Variablen in einem Ausdruck werden wir im Rahmen des Lambda-Kalküls noch formal definieren. 2 http://en.wikipedia.org/wiki/Monad_(category_theory) 3 Das Wort Monade kommt aus dem Griechischen und bedeutet Eines. Zu Leibniz’ Idee der Monade: „Eine Monade – der zentrale Begriff der Leibnizschen Welterklärung – ist eine einfache, nicht ausgedehnte und daher unteilbare Substanz, die äußeren mechanischen Einwirkungen unzugänglich ist. Das gesamte Universum bildet sich in den von den Monaden spontan gebildeten Wahrnehmungen (Perzeptionen) ab. Sie sind eine Art spirituelle Atome, ewig, unzerlegbar, einzigartig.“ – nach http://de.wikipedia.org/wiki/Leibniz 1 Die do-Notation ist in Haskell syntaktischer Zucker für alle Monaden, IO ist Instanz der Klasse Monad. Nun stellt sich die Frage, ob es noch weitere Instanzen dieser Klasse gibt. Die Maybe-Monade Auch Maybe ist ein einstelliger Typkonstruktor und es war auch sinnvoll eine Instanz der Klasse Functor zu definieren. Entsprechend ist Maybe auch eine sinnvolle Instanz der Klasse Monad. data Maybe a = Nothing | Just a Wir betrachten noch einmal die Auswertung arithmetischer Ausdrücke: data Expr = Expr :+: Expr | Expr : / : Expr | Num Float Problem ist hier die Vermeidung von Laufzeitfehlern (hier beim Teilen durch Null), Ziel ist eine Funktion eval mit folgenden Eigenschaften: eval (Num 3 :+: Num 4 ) -> Just 7.0 eval (Num 3 :/: (Num (-1) :+: Num 1)) -> Nothing Dazu definieren wir nun die Funktion eval: eval :: Expr -> Maybe Float eval (Num n) = Just n eval (e1 :+: e2) = case eval e1 of Nothing -> Nothing Just n1 -> case eval Nothing Just n2 eval (e1 :/: e2) = case eval e2 of Nothing -> Nothing Just 0 -> Nothing Just n2 -> case eval Nothing Just n1 e2 of -> Nothing -> Just (n1 + n2) e1 of -> Nothing -> Just (n1 / n2) Dies geht einfacher und schöner mit Maybe als Monade, wobei ein Fehler „durchschlägt“: instance Monad Maybe where Nothing >>= k = Nothing Just x >>= k = k x return = Just fail _ = Nothing 2 Nun kann man den Auswerter einfacher definieren: eval (Num n) = return n eval (e1 :+: e2) = do n1 <- eval e1 n2 <- eval e2 return (n1 + n2 ) eval (e1 :/: e2) = do n2 <- eval e1 if n2==0 then Nothing else do n2 <- eval e2 return (n1/n2) Die Monadengesetze können für Maybe einfach nachgewiesen werden (Übung). Die Monade mit dem Plus Eine andere Sicht auf den Datentyp Maybe ist, dass Maybe eine Struktur (ein Container) ist, die null oder ein Element aufnehmen kann. Wenn man dies auf Listen als Container verallgemeinert, kann man auch für den Listentyp eine Monadeninstanz definieren: instance Monad [] where return x = [x] -- return = (:[]) (x:xs) >>= f = f x ++ (xs >>= f) [] >>= f = [] -- (>>=) = flip concatMap fail _ = [] Dann ist [1,2,3] >>= \x -> [4,5] >>= \y -> return (x, y) ~> [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)] Dies ist übersetzt in die do-Notation: do x <- [1,2,3] y <- [4,5] return (x, y) Diese Darstellung erinnert stark an die List Comprehensions, welche tatsächlich nur syntaktischen Zucker für die Listenmonade darstellen: 3 [ (x, y) | x <- [1,2,3], y <- [4,5] ] Eine weitere Eigenschaft der Listenmonade ist, dass die leere Liste die Null der Struktur ist, da gilt: m >>= \_ -> [] = [] [] >>= \_ -> m = [] Des Weiteren gibt es eine ausgezeichnete, assoziative Funktion (++) mit (m ++ n) ++ o = m ++ (n ++ o) Zusammengefasst ergibt sich die Klasse MonadPlus: class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a Für Listen ergibt sich dann: instance MonadPlus [] where mzero = [] [] `mplus` ys = ys (x:xs) `mplus` ys = x : (xs `mplus` ys) -- mplus = (++) Auch für den Datentyp Maybe können wir eine Instanz angeben: instance MonadPlus Maybe where mzero = Nothing Nothing `mplus` m = m (Just x) `mplus` _ = Just x Bevor wir uns weiter mit der Idee speziell der Listenmonade beschäftigen, kommen wir noch einmal zurück zu den Monaden, für welche viele weitere sinnvolle Funktionen definiert werden können. Wir stellen hier nur die wichtigsten Funktionen vor. sequence :: Monad m => [m a] -> m [a] sequence = foldr mcons (return []) where mcons p q = p >>= \x -> q >>= \ys -> return (x:ys) sequence_ :: Monad m => [m a] -> m () sequence_ = foldr (>>) (return ()) Nun kann man beispielsweise benutzen: 4 sequence [getLine, getLine] >>= print sequence [[1,2],[3,4]] ~> [[1,3],[1,4],[2,3],[2,4]] Wir können nun map auf Monaden definieren: mapM :: Monad m => (a -> m b) -> [a] -> m [b] mapM f as = sequence (map f as) mapM_ :: Monad m => (a -> m b) -> [a] -> m () mapM_ f a = sequence_ (map f as) Nun verwenden wir: mapM_ putStr ["Hallo ", "Leute"]) ~> Hallo Leute mapM (\str -> putStr (str ++ ": ") >> getLine) ["Vorname", "Name"] >>= print ~> Vorname: $Frank$ Name: $Huch$ ["Frank","Huch"] Außerdem ist es für Monaden möglich ein if-then ohne else zu definieren: when :: Monad m => Bool -> m () -> m () when b a = if b then a else return () main = do str <- getLine when (str == "Frank") (putStrLn "Hi Frank, nice to meet you") ... Suchen mit der Listenmonade Wir haben schon gesehen, dass die Listenmonade und List Comprehensions eigentlich die gleiche Struktur sind. Es fehlt aber noch die Möglichkeit booleschen Bedingungen zu formulieren, welche das Ergebnis einschränken. Hierzu definiert man eine Funktion guard, welche den Suchraum einschränkt und für Listen wir folgt definiert werden kann: guard :: Bool -> [()] guard False = [] guard True = [()] Diese Definition erscheint zunächst wenig sinnvoll, ist es aber in Kombination mit dem bind Operator. Das folgende Beispiel zeigt dies. Der Ausdruck 5 return False >>= guard >> return 42 wird zu guard False >> return 42 = [] >> return 42 = [] ausgewertet, der Ausdruck return True >>= guard >> return 42 aber zu guard True >> return 42 = [()] >> return 42 = return 42 = [42] Mit guard kann man also Ergebnisse anhand eines Prädikats verwerfen. Das erinnert nicht zufallig an die filter-Funktion, die man mit guard definieren kann: filter :: (a -> Bool) -> [a] -> [a] filter p xs = do x <- xs guard (p x) return x Üblicherweise verwendet man guard um zulässige Ergebnisse aus einem größeren Suchraum auszuwählen. Zum Beispiel könnten wir ein Programm zur Berechnung Pythagoräischer Tripel wie folgt schreiben: pytriples :: Int -> [(Int,Int,Int)] pytriples max = do a <- [1..max] b <- [a..max] c <- [b..max] guard (a*a + b*b == c*c) return (a,b,c) Der Aufruf pytriples 10 ergibt dann [(3,4,5),(6,8,10)]. Tatsächlich kann man beliebige List-Comprehensions in do-Notation übersetzen (siehe Übung). Die guard-Funktion ist für beliebige Instanzen der Klasse MonadPlus wie folgt vordefiniert: 6 guard :: MonadPlus m => Bool -> m () guard False = mzero guard True = return () Man könnte also eine (leicht angepasste) Version der pytriples-Funktion auch in anderen MonadPlus-Instanzen ausführen. Die MonadPlus-Gesetze Bisher haben wir die MonadPlus-Gesetze nur für die Listeninstanz formuliert. Allgemein lauten sie wie folgt: m >>= \_ -> mzero = mzero mzero >>= \_ -> m = mzero (m `mplus` n) `mplus` o = m `mplus` (n `mplus` o) mzero und mplus formen also einen Monoid. Es gibt aber noch eine weitere wünschenswerte Eigenschaft von MonadPlus-Instanzen Für jede Funktion f sollte (>>= f) verknüpfungstreu bezüglich mzero und mplus (also gewissermaßen ein MonadPlus-Homomorphismus) sein: mzero >>= f (a `mplus` b) >>= f = = mzero (a >>= f) `mplus` (b >>= f) Diese Gesetze stellen sicher, dass während einer Berechnung keine Ergebnisse verloren gehen, sind aber nicht für jede vordefinierte MonadPlus-Instanz erfüllt. Zum Beispiel erfüllt die Instanz für Maybe das zweite, sogenannte Distributivgesetz für MonadPlus, nicht, denn es gilt für a = return False, b = return True und f = guard: = = = = (return False `mplus` return True) >>= guard (Just False `mplus` Just True) >>= guard Just False >>= guard guard False Nothing aber (return False >>= guard) `mplus` (return True >>= guard) = guard False `mplus` guard True = Nothing `mplus` Just () = Just () 7 Die Maybe-Monade ist daher nur bedingt zum Lösen von Suchproblemen geeignet, da Ergebnisse verloren gehen können. Wenn man zum Beispiel, wie oben beschrieben, Pythagoräische Tripel in der Maybe-Monade berechnen möchte, erhält man als Ergebnis Nothing statt Just (3,4,5). 8