ALP I Monaden in Haskell Teil I WS 2009/2010 Prof. Dr. Margarita Esponda Prof. Dr. Margarita Esponda 1 Schönheit der funktionalen Programmiersprachen Schöne Eigenschaften von Programmen in rein funktionalen Programmiersprachen sind: • Semantische Korrespondenz zwischen ProgrammFunktionen und mathematischen Funktionen • Funktionale Sprachen sind deklarativ Church-Rosser-Eigenschaft • Funktionen Höherer Ordnung • Algebraische Datentypen • Parametrischer Polymorphismus • Abstrakte Datentypen Prof. Dr. Margarita Esponda 2 Vorteile von funktionalen Programmiersprachen Vorteile von Programmen in rein funktionalen Programmiersprachen sind: • Programme sind sehr kompakt • Mathematische Analyse von Programmeigenschaften ist viel leichter als in imperativen Programmiersprachen. • Programmverifikation ist machbar • Saubere Schnittstellen • Programme haben keine Seiteneffekte Prof. Dr. Margarita Esponda 3 Nachteile von funktionalen Programmiersprachen Nachteile: Rein funktionale Programmiersprachen: • kein "up-date in place" wie in imperativen Programmiersprachen => Programme sind langsamer und brauchen mehr Speicher. • keine Ein-/Ausgabe während der Ausführung des Programm (nicht interaktiv) • keine Fehlerbehandlung • keine Nebenläufigkeit • keine Zustände oder Programm-Tracing möglich • keine Schnittstellen zu anderen Programmiersprachen Prof. Dr. Margarita Esponda 4 Wo liegt das Hauptproblem? Wie können Programme in funktionalen Programmiersprachen mit der Welt interagieren, die die Welt zwischendurch verändert, ohne die Schönheit des funktionalen Programmierens zu zerstören? Hauptproblem Wie können funktionale Programmiersprachen mit Seiteneffekten am besten umgehen? Prof. Dr. Margarita Esponda 5 1. Lösung Wir können einfach Ein-/Ausgabe-Funktionen einführen. solution = readNum() + readNum()*2 Beispiele: Das Ergebnis hängt von der Auswertungsreihenfolge der (+)-Operation ab. list = [putChar 'a' + putChar 'b' ] Die putChar-Funktion wird in Abhängigkeit der Verwendung der Liste (list) ausgeführt oder nicht. Wenn nur die Länge der Liste berechnet wird, müssen die Elemente der Liste nicht ausgewertet werden. Prof. Dr. Margarita Esponda 6 Lazy-Evaluation Auswertung nach Bedarf Lazy-Evaluation ist inkompatibel mit Seiteneffekten Seiteneffekte Lazy-Evaluation Funktionale Programmierung Prof. Dr. Margarita Esponda 7 Wir können nicht auf Seiteneffekte verzichten Moderne Software ist interaktiv und verlangt anspruchvolle Benutzerschnittstellen. Das impliziert: • Fehlerbehandlung • Nebenläufigkeit • Verfolgung von Zuständen (für Fehlersuche) • Veränderungen am Ort (up-date in place), um effiziente Programme zu implementieren • Ein-/Ausgabe Prof. Dr. Margarita Esponda 8 Monaden Lösung: • Anfang der 90er Jahre • aus der Kategorientheorie • Monaden sind abstrakte Datentypen • Idee: Seitenefekte ja, aber unter strenger Kontrolle • Seiteneffekte können nur in bestimmten Teilen eines Programms entstehen. Der Rest bleibt rein funktional. Prof. Dr. Margarita Esponda 9 Vor Monaden gab es andere Lösungen • Streams • Continuations • World-Passing Prof. Dr. Margarita Esponda 10 Motivation Nehmen wir an, wir haben eine Funktion, die boolsche Ausdrücke auswertet. data BExp = T | F | And BExp BExp | Or BExp BExp | Not BExp deriving Show eval :: BExp -> Bool eval T = True eval F = False eval (And e1 e2) = (eval e1) && (eval e2) eval (Or e1 e2) = (eval e1) || (eval e2) eval (Not e) = not (eval e) Prof. Dr. Margarita Esponda 11 Motivation Wir möchten bei einer Auswertung die Reihenfolge der Operationen protokollieren. eval :: BExp -> Trace Bool eval T = (True, trace T True) Funktionen müssen komplett neu geschrieben werden ! Sehr umständlich ! eval F = (False, trace F False) eval e@(And e1 e2) = let (re1, te1) = (eval e1) in let (re2, te2) = (eval e2) in let r = re1 && re2 in (r, te1++te2++(trace e r)) eval e@(Or e1 e2) = let (re1, te1) = (eval e1) in let (re2, te2) = (eval e2) in let r = re1 || re2 in (r, te1++te2++(trace e r)) eval (Not e) = let (re, te) = (eval e) in let r = (not re) in (r, te++(trace (Not e) r)) trace bexp b = "eval (" ++ show bexp ++ ") =" ++ show b ++ "\n" Prof. Dr. Margarita Esponda 12 Motivation mit Fehlerbehandlung tail :: [a] -> [a] tail ( _ : xs ) = xs tail [] = error ("… empty list") divide :: Int -> Int -> Int divide n m | (m /= 0) | otherwise Prof. Dr. Margarita Esponda = n 'div' m = error ("… not posible!") 13 Motivation mit Fehlerbehandlung data Maybe a = Nothing | Just a deriving (Eq, Ord, Read, Show) errorDivide :: Int -> Int -> Maybe Int errorDivide n m | m == 0 = Nothing | otherwise errorTail :: [a] -> Maybe [a] errorTail ( _ : xs ) = Just xs errorTail [] Prof. Dr. Margarita Esponda = Just (n `div` m) = Nothing 14 Motivation mit Fehlerbehandlung Was passiert, wenn wir unsere Funktionen innerhalb anderer Funktionen verwenden? f ( errorDivide x y) Der Datentyp muss zusammenpassen! errorDivide :: f Int -> Int :: Maybe Int -> -> Maybe Int b Wir möchten den Fehler weiterleiten! Prof. Dr. Margarita Esponda 15 Motivation mit Fehlerbehandlung Maybe-Monade mapMaybe-Funktion Maybe b Maybe a g a b mapMaybe g Prof. Dr. Margarita Esponda 16 Motivation mit Fehlerbehandlung Maybe-Monade mapMaybe :: (a->b) -> Maybe a -> Maybe b mapMaybe g Nothing mapMaybe g Prof. Dr. Margarita Esponda = Nothing (Just x) = Just ( g x ) 17 Motivation mit Fehlerbehandlung Maybe-Monade maybe-Funktion b Maybe a f a n maybe n f Prof. Dr. Margarita Esponda 18 Motivation mit Fehlerbehandlung Maybe-Monade maybe :: b -> (a->b) -> Maybe a -> b maybe n f Nothing maybe n f = n (Just x) = f x Anwendung: f a b = maybe b (+a) (mapMaybe (*2) (errorDivide a b)) Prof. Dr. Margarita Esponda 19 Motivation mit Fehlerbehandlung Maybe-Monade Beispiel: f a b = maybe a (+b) (mapMaybe (*2) (errorDivide a b)) f 20 0 => maybe 20 (+0) (mapMaybe (*2) (errorDivide 20 0)) => maybe 20 (+0) (mapMaybe (*2) Nothing ) => maybe 20 (+0) ( Nothing ) => maybe 20 Prof. Dr. Margarita Esponda 20 Motivation mit Fehlerbehandlung Maybe-Monade Beispiel: f a b = maybe a (+b) (mapMaybe (*2) (errorDivide a b)) f 20 2 => maybe 20 (+2) (mapMaybe (*2) (errorDivide 20 2)) => maybe 20 (+2) (mapMaybe (*2) (Just 10)) => maybe 20 (+2) ( Just 20) => (+2) (Just 20) => 22 Prof. Dr. Margarita Esponda 21 Maybe-Monade data Maybe a = Nothing | Just a Definition der Maybe-Monade: instance Monad Maybe where Nothing (Just x) return Prof. Dr. Margarita Esponda >>= >>= f = Nothing f = f x = Just 22 Maybe-Monade maybe :: b -> (a -> b) -> Maybe a -> b isJust :: Maybe a -> Bool isNothing :: Maybe a -> Bool fromJust :: Maybe a -> a fromMaybe :: a -> Maybe a -> a listToMaybe :: [a] -> Maybe a maybeToList :: Maybe a -> [a] catMaybes :: [Maybe a] -> [a] mapMaybe :: (a -> Maybe b) -> [a] -> [b] Prof. Dr. Margarita Esponda 23 Maybe-Monade msqrt :: Float -> Maybe Float msqrt a | a<0 = Nothing msqrt a = Just (sqrt a) *Main> return 4 4 *Main> return 4 >>= msqrt Just 2.0 *Main> return 4 >>= msqrt >>= msqrt Just 1.4142135 *Main> Prof. Dr. Margarita Esponda 24 Maybe-Monade Beispiel: sqrtMaybe :: Int -> [Maybe Int] sqrtMaybe a | a >= 0 = [ Just ( round (sqrt (fromIntegral a))), Just ( -round (sqrt (fromIntegral a))) ] | otherwise = [Nothing] hugs> sqrtMaybe 3 [Just 2,Just (-2)] hugs> sqrtMaybe (-4) [Nothing] Prof. Dr. Margarita Esponda 25 Motivation Nehmen wir an, wir haben folgende zwei Funktionen: f :: a -> b g :: b -> c Wir können ohne weiteres eine Funktionskomposition h definieren h :: a -> c h =g.f Wenn wir ein Protokoll (trace) von den Funktionen haben möchten, müssen wir den Typ der Funktionen f, g wie folgt umdefinieren: f :: a -> ( b, String ) g :: b -> ( c, String ) Prof. Dr. Margarita Esponda 26 Motivation f :: a -> ( b, String ) g :: b -> ( c, String ) Die Funktion h muss wie folgt umdefiniert werden: h x = let (res_f, trace_f) = f x (res_g, trace_g) = g res_f in ( res_g, trace_g ++ trace_f ) Prof. Dr. Margarita Esponda 27 Motivation Eine allgemeine Funktion, die sich um diese Typanpassung kümmert wäre viel besser: bind :: (a, String) -> (a -> (b, String)) -> (b, String) bind (x, s) f = let (result_f, trace_f) = f x in ( result_f, s ++ trace_f ) h x = f x `bind` g f x = ( x, "f_called" ) g x = ( x, "g_called" ) h x = f `bind` g `bind` Prof. Dr. Margarita Esponda (/x -> (x, "end…") 28 Was sind Monaden? Monaden befreien den Programmierer von der Arbeit, den Datentyp der Funktionen, die man kombiniert, anzupassen. Sind eine Art Verpackung (Wrapper) für andere Datentypen. Mit Monaden kann eine kontrollierte Pipeline zwischen Funktionen gebildet werden. Prof. Dr. Margarita Esponda 29 Monaden Monaden bestehen im Allgemeinen aus: -- einem Monaden-Typ M data M a = ... -- einem Typ-Konstruktor, der eine Monade erzeugt return :: a -> M a -- einer Verbindungsfunktion, die eine Art Pipeline zwischen -- den Funktionen ermöglicht (>>=) :: M a -> (a -> M b) -> M b Prof. Dr. Margarita Esponda 30 Ein-/Ausgabe-Monaden I/O-Monaden Der Wert einer Ein-/Ausgabe-Monade (IO a) stellt eine Aktion dar. Wenn die Aktion ausgeführt wird, kann die Ein-/Ausgabe erfolgen, bevor ein Ergebnis von Typ a zurückgeliefert wird. type IO a = World -> ( a, World ) Prof. Dr. Margarita Esponda 31 Ein-/Ausgabe-Monaden ergebnis :: a Welt vor IO a Welt nach der Aktion der Aktion kein Rückgabewert main :: IO () Die main-Funkion ist eine Aktion von Typ-IO () main = putChar ‘x’ Prof. Dr. Margarita Esponda 32 do-Anweisung unit Char getChar putChar main :: IO () main () Char = do c <- getChar Sequenzialisiert die Ausführung der Anweisungen putChar 'c' Prof. Dr. Margarita Esponda 33 Verbindung der Aktionen () Char getChar putChar Um ein Zeichen zu lesen und wieder auszugeben, müssen wir die Aktionen verbinden. (>>=) :: IO a -> (a -> IO b) -> IO b Verbindungsoperator Prof. Dr. Margarita Esponda 34 (>>=) bind-Kombinator • verbindet das Ergebnis der linken Aktion mit der Aktion auf der rechten Seite des Ausdrucks • (>>=) Funktionskomposition zwischen zwei Aktionen • Beispiel: f >>= g • führt die Funktion f aus und produziert das Ergebnis r1 • verwendet wiederum die Funktion g auf r1 und produziert r2. f Prof. Dr. Margarita Esponda r1 g r2 35 (>>=) bind-Kombinator Beispiel: echoTwice :: IO () echoTwice = getChar >>= (\c ->putChar c >>= (\() -> putChar c )) (>>) then-Kombinator realisiert nur eine Sequenzialisierung ohne Parameterübergabe echoTwice :: IO () echoTwice = getChar >>= \c ->putChar c >> putChar c Prof. Dr. Margarita Esponda 36 return-Kombinator Die (return value) Aktion hat keine Ein-/Ausgabe und gibt einen Wert zurück. return :: a -> IO a return getTwoChars :: IO (Char, Char) getTwoChars = getChar >>= \c1 -> getChar >>= \c2 -> return (c1,c2) Prof. Dr. Margarita Esponda 37 do-Anweisung -- ohne do-Anweisung getTwoChars :: IO (Char,Char) getTwoChars = getChar >>= \c1 -> getChar >>= \c2 -> return (c1,c2) -- mit do-Anweisung getTwoChars :: IO(Char,Char) getTwoChars = do { c1 <- getChar ; c2 <- getChar ; return (c1,c2) } Prof. Dr. Margarita Esponda 38 do-Anweisung Folgende Anweisungen sind äquivalent: do { x1 <- p1; ...; xn <- pn; q } do x1 <- p1; ...; xn <- pn; q do x1 <- p1 ... xn <- pn q Prof. Dr. Margarita Esponda mit Einrücken 39 do-Anweisung getLine :: IO String getLine = do c <- getChar if c == '\n' then return "" else do line <- getLine return (c: line) Prof. Dr. Margarita Esponda 40 Monaden Die Monad-Klasse class Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b fail :: String -> m a return :: a -> m a Prof. Dr. Margarita Esponda 41