Funktoren I/O Monadische Funktionen Einführung in die Funktionale Programmierung mit Haskell Input/Output Steffen Jost LFE Theoretische Informatik, Institut für Informatik, Ludwig-Maximilians Universität, München 20. Juni 2013 Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Planung Freitag 21.Juni: keine Übung Mittwoch 26.Juni: Übung statt Vorlesung Klausur: 19.7. 10h Vorläufige Planung: Mi Vorlesung Do Vorlesung 17. Apr 13 18. Apr 13 24. Apr 13 25. Apr 13 1. Mai 13 2. Mai 13 8. Mai 13 9. Mai 13 15. Mai 13 16. Mai 13 22. Mai 13 23. Mai 13 29. Mai 13 30. Mai 13 5. Jun 13 6. Jun 13 12. Jun 13 13. Jun 13 19. Jun 13 20. Jun 13 26. Jun 13 27. Jun 13 3. Jul 13 4. Jul 13 10. Jul 13 11. Jul 13 17. Jul 13 18. Jul 13 Fr Übung 19. Apr 13 26. Apr 13 3. Mai 13 10. Mai 13 17. Mai 13 24. Mai 13 31. Mai 13 7. Jun 13 14. Jun 13 21. Jun 13 28. Jun 13 5. Jul 13 12. Jul 13 19. Jul 13 Steffen Jost Sollumfang: Raumbuchung: 3+2 4+2 42 Termine, davon 3 Feiertage 4 Übung nachholen 6 Entfällt 1 Klausurtermin = 20V + 12Ü Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Typklasse Functor Im Module Data.Functor findet sich folgende Definition: class Functor f where fmap :: (a -> b) -> f a -> f b Damit können wir ganz generisch Funktionen anwenden, deren Argumenttyp eigentlich in einen anderen Typ eingewickelt wurde: data Maybe a = Nothing | Just a instance Functor Maybe where fmap f (Just x) = Just (f x) fmap _ Nothing = Nothing > fmap (+1) (Just 2) Just 3 > fmap (+1) Nothing Nothing Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Typklasse Functor Im Module Data.Functor findet sich folgende Definition: class Functor f where fmap :: (a -> b) -> f a -> f b Damit können wir ganz generisch Funktionen anwenden, deren Argumenttyp eigentlich in einen anderen Typ eingewickelt wurde: data Maybe a = Nothing | Just a instance Functor Maybe where fmap f (Just x) = Just (f x) fmap _ Nothing = Nothing Der Parameter f der Typklasse Functor steht also nicht für einen konkreten Typ wie z.B. Maybe Int, sondern für einen Typkonstruktor wie z.B. Maybe. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Funktoren Beispiele class Functor f where fmap :: (a -> b) -> f a -> f b (<$>) :: (a -> b) -> f a -> f b (<$>) = fmap -- nur ein Synonym in Infix Notation Wir können Funktion auch punktweise auf Listen anwenden: instance Functor [] where fmap = map oder auch auf Bäume: data Tree a = Leaf a | Node (Tree a) a (Tree a) instance Functor Tree where fmap f (Leaf a) = Leaf (f a) fmap f (Node l a r) = Node (f <$> l) (f a) (f <$> r) Für die Funktion fmap gibt es auch das Infix Synonym <$> Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Funktoren In der Mathematik ist ein Funktor eine strukturerhaltende Abbildung zwischen zwei Kategorien. fmap :: (a -> b) -> f a -> f b In Haskell nimmt fmap eine Abbildung von Typ a nach Typ b und liefert eine Abbildung auf Typen, auf welche jeweils der Typkonstruktor f angewendet wurde. Man sagt auch fmap f wendet die Funktion f punktweise an. Ein Funktor sollte folgende Gesetze erfüllen: 1 Identität: fmap id == id 2 Komposition: fmap f . fmap g == fmap (f.g) Haskell kann diese Gesetze leider jedoch nicht erzwingen, dass muss der Programmierer alleine sicherstellen. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen I/O vs. Haskell Rein Funktionale Welt: Haskell Funktionen sind rein und kennen keine Seiteneffekte. ⇒ Ein Aufruf von fmap löscht nicht die Festplatte. ⇒ Funktionen liefern das gleiche Ergebnis für gleiche Argumente. ⇒ Haskell Code ist leichter zu verstehen und sehr modular. versus Input/Output: Input/Output besteht nur aus Seiteneffekten! Eine Funktion, welche einen String auf den Bildschirm ausgibt hat nur den Seiteneffekt, den Bildschirm zu verändern. Schlimmer: Funktionen, welche von Tastatur einlesen, haben keine Argumente und liefert oft andere Ergebnisse! Noch schlimmer: I/O-Funktionen sollen die Festplatte löschen können! Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Haskell’s I/O Haskell versucht nicht das Rad neu zu erfinden: Input/Output ist am einfachsten im imperativen Stil durchzuführen, auch in Haskell! Haskell verrät seine Grundprinzipien dennoch nicht: Bei allem schmutzigen ein/aus bleibt Haskell trotzdem rein! Haskell macht es mit Monade: Die Lösung liegt im Konzept der Monade. Dieses Konzept finden viele schwierig, weshalb wir uns zuerst an die Verwendung von Monaden im Speziallfall der IO-Monade gewöhnen wollen, bevor wir das abstrakte Konzept untersuchen. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Haskell verändert die Welt: Damit eine rein Funktionale Ein-/Ausgabe Funktion die Welt verändern kann, muss die Welt als Argument übergeben werden. Die veränderte Welt wird von der Funktion als Ergebnis mit zurückgegeben: type IO a = Welt -> (a,Welt) putStr getLine :: String -> IO () :: IO String -- String -> Welt -> ((),Welt) -Welt -> (String,Welt) Damit ist schon mal klar, das die Reihenfolge mehrerer I/O-Funktionen eingehalten werden muss: Die veränderte Welt der einen Funktion, wird immer weiter an die nächste Übergeben! Hinweis: Der Typkonstruktor IO ist nicht wirklich so definiert, aber man könnte es so definieren — und wir können es uns so vorstellen! Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Übungsaufgabe Schreiben Sie zur Übung die beiden Funktionen nacheinander und komposition mit folgenden Typsignaturen: type Welt = () type IO a = Welt -> (a,Welt) nacheinander :: IO a -> IO b -> IO b komposition :: IO a -> (a -> IO b) -> IO b Nacheinander führt einfach zwei IO-Aktionen hintereinander aus. Dazu muss aus dem Ergebnis der ersten Aktion die verändert Welt ausgepackt und als Argument für die zweite Aktion übergeben werden. Man muss die Welt also durchfädeln (engl. threading). Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation Um mehrere IO-Aktionen hintereinander auszuführen, gibt es in Haskell die imperativ anmutende DO-Notation: voo :: IO () voo = do putStr "Hallo " putStrLn "Welt!" In jeder Zeile darf eine IO-Aktion stehen. Eine “IO-Aktion” ist ein Ausdruck des Typs IO a. Der Typparameter für IO darf sich von Zeile zu Zeile unterscheiden; also z.B. IO Int, IO Bool,. . . DO-Ausdrücke sind durch Einrückung begrenzt: steht etwas weiter links als die erste IO-Aktion, ist der DO-Block beendet. Gesamter DO-Ausdruck hat den Typ des letzten Ausdrucks. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation (2) Alle IO-Aktionen haben ein Ergebnis (neben der veränderten Welt): Eine IO-Aktion des Typs IO a liefert einen Wert des Typs a. Rückgabewerte können wir mit Rückpfeil <- an Variablen binden: voo :: IO String voo = do putStrLn "Ihr Name bitte: " name <- getLine _ <- putStr "Sie heissen " putStr name putStrLn "?" getLine Wenn uns das Ergebnis der IO-Aktion egal ist, dann können wir den Rückpfeil auch einfach weglassen. Nur bei der letzten Aktion dürfen wir keinen Rückpfeil verwenden — das Ergebnis ist ja das Ergebnis der Funktion voo. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation (3) Falls eine IO-Aktion Argumente verlangt, z.B. putStrLn :: String -> IO () so können wir diese normal funktional bearbeiten: voo :: IO String voo = do putStrLn "Ihr Name bitte: " name <- getLine putStrLn ("Sie also heissen " ++ name ++ " ?" ++ sicher) getLine where sicher = "\nSind Sie sich sicher?" Ein DO-Block ist wirklich nur ein Ausdruck des Typs IO a! ’\n’ :: Char ist das Zeichen für Zeilchenvorschub Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation (3) Falls eine IO-Aktion Argumente verlangt, z.B. putStrLn :: String -> IO () so können wir diese normal funktional bearbeiten: voo :: IO String voo = let sicher = "\nSind Sie sich sicher?" in do putStrLn "Ihr Name bitte: " name <- getLine putStrLn $ "\Sie also heissen " ++ name ++ " ?" ++ sicher) getLine Ein DO-Block ist wirklich nur ein Ausdruck des Typs IO a! ’\n’ :: Char ist das Zeichen für Zeilchenvorschub Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation (4) Ähnlich zu List-Comprehensions, können wir mit let auch wieder rein funktionale Abkürzungen definieren: voo :: IO String voo = do let begrüssung = "Ihr Name bitte: " putStr begrüssung name <- getLine let frage = "Sie also heissen " ++ ++ (map Data.Char.toUpper name) ++ " ?" ++ sicher sicher = "\nSind Sie sich sicher?" putStr frage getLine let ist wieder durch Einrückung begrenzt Sowohl let als auch <- können Pattern-Matching verwenden – genau wie in List-Comprehensions auch Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Main Methode Bisher haben wir Haskell nur im Interpreter laufen lassen. Wenn wir eine ausführbare Datei erstellen wollen, dann braucht unser Haskell Programm eine Funktion mit dem Namen main: helloworld.hs: main = putStrLn "Hello World!" > ghc helloworld.hs [1 of 1] Compiling Main Linking helloworld ... > ./helloworld Hello World! ( helloworld.hs, helloworld.o ) Tipp: Besteht ein Programm aus mehreren Modulen, dann sollte die Option --make angegeben werden, damit alle notwendigen Module ebenfalls gleich kompiliert werden. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Main Methode Die main Funktion hat meist den Typ IO () Natürlich können wir auch in der Funktion main mehrere IO-Aktionen durchführen: main = do putStrLn "Psst! Wie heisst Du?" name <- getLine putStrLn $ "Hey " ++ (map Data.Char.toUpper) name voo :: String -> IO () voo name = putStrLn $ name ++ " ist doof!" Nur main verfügt über die Welt. Die IO-Aktionen in voo sind ohne Wirkung, da main die Funktion voo nicht aufruft! Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Main Methode Wenn main aber voo aufruft, dann werden dessen Aktionen an der entsprechenden Stelle ausgeführt. Natürlich kann voo weitere Funktionen mit Typ IO a aufrufen. main = do putStrLn "Psst! Wie heisst Du?" name <- getLine voo name putStrLn $ "Hey " ++ (map Data.Char.toUpper) name voo :: String -> IO () voo name = putStrLn $ name ++ " ist doof!" In jedem Falle gilt: Man kann am Typ einer Funktion erkennen ob diese Seiteneffekte haben kann – und wie wir später sehen werden kann der Typ auch Einschränken welche Art Seiteneffekte. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Trennung der Welten Es empfiehlt sich jedoch sehr dringend, funktionalen Code von I/O-Code so weit wie möglich zu trennen: main = do line <- getLine if null line then putStrLn "(no input)" else do putStrLn $ reverseWords line main reverseWords :: String -> String reverseWords = unwords . map reverse . words DO-Ausdrücke können wie alle anderen Ausdrücke überall auftauchen, wo Ihr Typ gefragt ist. Auch IO-Aktionen können Rekursion nutzen Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Ausgabe putChar :: Char -> IO () Gibt ein einzelnes Zeichen aus. putStr :: String -> IO () putStrLn :: String -> IO () -- fügt ’\n’ ans Ende an Gegeben einen String aus. Aufgrund der verzögerten Auswertung kann es sein, dass nur komplette Zeilen ausgegeben werden. print :: Show a => a -> IO () Gibt einen Wert der Typklasse Show aus. Identisch zu putStrLn . show Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen GHCi und I/O Der Interpreter GHCi erlaubt an der Eingabeaufforderung übrigens auch beliebige IO-Aktionen. In der Tat wird auf das Ergebnis einer jeden Eingabe ohnehin die Funktion print angewendet: > [1..5] [1,2,3,4,5] it :: [Integer] > print [1..5] [1,2,3,4,5] it :: () Lediglich das Ergebnis ändert sich, da print ja den Typ Show a => a -> IO () hat. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Eingabe getChar :: IO Char Liest ein einzelnes Zeichen ein. getLine :: IO String Liest so lange ein, bis ein Zeilchenvorschub durch drücken der Return-Taste erkannt wird. getContents :: IO String Liest alles ein, was der Benutzer jemals eingeben wird (oder bis ein Dateiende-Zeichen (Ctrl-D) erkannt wird ) interact :: (String -> String) -> IO () Verarbeitet den gesamten Input mit der gegebenen Funktion und gibt das Ergebnis zeilenweise aus. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen getContents Die Funktion getContents :: IO String liest die gesamte Benutzereingabe auf einmal ein. Aufgrund der verzögerten Auswertung von Haskell macht dies Sinn: import Data.Char main = do input <- getContents let shorti = shortLinesOnly input bigshort = map toUpper shorti putStr bigshort shortLinesOnly :: String -> String shortLinesOnly = unlines . shortfilter . lines where shortfilter = filter (\line -> length line < 11) Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen interact Die Funktion interact :: (String -> String) -> IO () erlaubt uns, dies noch knapper auszudrücken: import Data.Char main = interact mangleinput where mangleinput = (map toUpper) . shortLinesOnly shortLinesOnly :: String -> String shortLinesOnly = unlines . shortfilter . lines where shortfilter = filter (\line -> length line < 11) Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation (5) Nachtrag Aufgrund der letzten Übungserfahrung wollen wir noch mal den Unterschied zwischen <- und let verdeutlichen. Was gibt dieses Programm aus? main = do putStrLn “A:“ a2 <- getLine b1 <-putStrLn “B“ let b2 = getLine let c1 = putStrLn “C“ c2 <- getLine putStrLn $ “A:“++a2++“ B:“++b2++“ C:“++c2 Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen DO-Notation (5) Nachtrag Aufgrund der letzten Übungserfahrung wollen wir noch mal den Unterschied zwischen <- und let verdeutlichen. Was gibt dieses Programm aus? main = do putStrLn “A:“ a2 <- getLine b1 <-putStrLn “B“ let b2 = getLine let c1 = putStrLn “C“ c2 <- getLine c1 b2 <- b2 putStrLn $ “A:“++a2++“ B:“++b2++“ C:“++c2 Antwort: A:a B:c C:b <- führt IO-Aktion sofort durch. let führt keine Aktion durch, kürzt nur ab. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Dateizugriff Das Modul System.IO bietet Varianten der IO-Funktionen für den Zugriff auf verschiedene Ein-/Ausgabegeräte an. Die Varianten erwarten ein zusätzliche Argument des Typs Handle: hPutStr hPutStrLn hPrint hGetLine hGetContents :: Handle -> :: Handle -> :: Show a => Handle -> :: Handle -> :: Handle -> String -> IO () String -> IO () a -> IO () IO String IO String Das Modul exportiert auch stdin,stdout,stderr :: Handle. Es gilt: putStr = hPutStr stdout getLine = hGetLine stdin Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Datei Handles Handles kann man auf verschiedene Arten bekommen: readFile :: FilePath -> IO String. writeFile :: FilePath -> String -> IO () appendFile :: FilePath -> String -> IO () openFile :: FilePath -> IOMode -> IO Handle hClose :: Handle -> IO () withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a type FilePath = String ⇒ Betriebssystem abhängig writeFile löscht Datei beim Öffnen data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode deriving (Enum,Ord,Eq,Show, withFile schließt die Datei in jedem Falle Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Beispiele Dateizugriff: Beispiel 1 import System.IO import Data.Char main = do contents <- readFile "whisper.txt" writeFile "shout.txt" (map toUpper contents) Beispiel 2 main = do hIn <- openFile "whisper.txt" ReadMode -- Öffnen hOut <- openFile "shout.txt" WriteMode -- Arbeiten input <- hGetContents hIn let biginput = map toUpper input hPutStrLn hOut biginput hClose hIn -- Schliessen hClose hOut Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Beispiele Dateizugriff: Beispiel 1 import System.IO import Data.Char main = do contents <- readFile "whisper.txt" writeFile "shout.txt" (map toUpper contents) Beispiel 2 main = do hIn <- openFile "whisper.txt" ReadMode -- Öffnen hOut <- openFile "shout.txt" WriteMode -- Arbeiten input <- hGetContents hIn let biginput = map toUpper input hPutStrLn hOut biginput hClose hIn -- Schliessen hClose hOut Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Beispiele Dateizugriff: Beispiel 3 main = do withFile "something.txt" ReadMode (\handle -> do contents1 <- hGetContents handle let contents2 = foo contents1 putStr contents2 ) Dabei kann man sich withFile vorstellen als: withFile’ :: FilePath -> IOMode -> (Handle -> IO a) -> IO a withFile’ path mode f = do handle <- openFile path mode result <- f handle hClose handle return result Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Grundidee Main IO-Funktionen Datei Operationen Verfeinerter Zugriff Positionieren hGetPosn :: Handle -> IO HandlePosn hSetPosn :: HandlePosn -> IO () hSeek :: Handle -> SeekMode -> Integer -> IO () Nicht alle Handle-Arten unterstützen Positionierung Datentyp HandlePosn nur in Typklassen Eq und Show. Kann man sich also nur merken und wiederverwenden. Pufferung Man kann üblicherweise auch die Pufferung beeinflussen. hFlush erzwingt das leeren des Puffers. hSetBuffering :: Handle -> BufferMode -> IO () hGetBuffering :: Handle -> IO BufferMode hFlush :: Handle -> IO () Warnung: Die vorgestellten Funktionen nur für einfach I/O-Aufgaben verwenden. Bei großen Dateien bzw. Bechmarks (!) sollte Modul Data.Bytestring verwendet werden. Nahezu identische Funktionen mit Bytestring anstatt String. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Monadische Funktionen Wir stellen nun einige wichtige grundlegende Funktionen aus dem Standardmodul Control.Monad vor. Die Funktionen sind nicht nur für I/O nützlich, sondern allgemein für beliebige Monaden. Für unsere Zwecke reicht aber erst einmal die Verwendung innerhalb der I/O-Monade. Den Typ foo :: Monad m => a -> m b lesen wir einfach als foo :: a -> IO b Wir bemerken aber bereits, dass der Typklasse Monad lediglich Typkonstruktoren angehören, ganz ähnlich wie bei Typklasse Functor. Typkonstruktoren: IO, Maybe, [], Tree, . . . Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen return Die Funktion return :: a -> IO a generiert eine “leere” IO-Aktion, welche die Welt nicht verändert und das Argument unverändert zurückgibt. Dies ist nützlich, wenn wir von dem Typ her eine IO-Aktion brauchen, aber eigentlich keine durchführen wollen: main = do line <- getLine if null line then return () else do putStrLn $ reverseWords line main Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen return Die Funktion return :: a -> IO a generiert eine “leere” IO-Aktion, welche die Welt nicht verändert und das Argument unverändert zurückgibt. Es ist auch nützlich, wenn wir das Ergebnis einer anderen IO-Aktion noch weiterverarbeiten wollen: getUpperLine :: IO String getUpperLine = do line <- getLine return $ map toUpper line Bemerke: Ausdruck map toUpper line hat den Typ String, wir brauchen am Ende dieses DO-Blocks den Typ IO String Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Alle Monaden sind Funktoren Damit ist auch sofort klar, wie wir den Typkonstruktor IO zum Funktor machen können: instance Functor IO where fmap f action = do result <- action return (f result) Wir führen die Aktion aus, und wenden danach die Funktion an. Die “Welt” heben wir während der Anwendung von f auf und geben sie am Ende mit return wieder dazu — eine rein funktionale Funktion konnte die Welt ja nicht verändern! getUpperLine = map toUpper <$> getLine map toUpper :: String -> String getLine :: IO String <$> :: (a -> b) -> f a -> f b In der Tat sind alle Monaden auch Funktoren. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Alle Monaden sind Funktoren Damit ist auch sofort klar, wie wir den Typkonstruktor IO zum Funktor machen können: instance Functor IO where fmap f action = do result <- action return (f result) Wir führen die Aktion aus, und wenden danach die Funktion an. Die “Welt” heben wir während der Anwendung von f auf und geben sie am Ende mit return wieder dazu — eine rein funktionale Funktion konnte die Welt ja nicht verändern! getUpperLine = map toUpper <$> getLine map toUpper :: String -> String getLine :: IO String <$> :: (a -> b) -> f a -> f b In der Tat sind alle Monaden auch Funktoren. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen when & unless Bedingte Ausführung von IO-Aktionen erlauben uns when :: Bool -> IO () -> IO () unless :: Bool -> IO () -> IO () Die Funktion when führt die übergebene Aktion nur dann aus, wenn das erste (funktionale) Argument zu True auswertet. Bei der Funktion unless ist das umgekehrt. Sie führt die übergebene Aktion nur dann aus, wenn das erste (funktionale) Argument zu False auswertet. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen forever Wir können eine IO-Aktion mit der Funktion forever bis zum Programmabbruch wiederholen lassen: forever :: IO a -> IO b Beispiel import Control.Monad import Data.Char main = forever $ do putStr "Give me some input: " l <- getLine putStrLn $ map toUpper l Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen sequence Mehrere IO-Aktionen können wir hintereinander ausführen lassen: sequence :: [IO a] -> IO [a] sequence_ :: [IO a] -> IO () Beispiel > sequence $ map print [1..5] 1 2 3 4 5 [(),(),(),(),()] Wenn das aufgesammelte Ergebnis nicht interessiert, z.B. weil die verwendeten IO-Aktion wie im Beispiel immer nur () zurückgeben, dann können wir auch die Variante sequence_ verwenden. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen mapM Wir können eine Sequenz von IO-Aktionen vorher auch noch transformieren lassen: mapM :: (a -> IO b) -> [a] -> IO [b] mapM_ :: (a -> IO b) -> [a] -> IO () Dabei ist mapM f ist tatsächlich äquivalent zu sequence . map f Beispiel > mapM_ print [1..5] 1 2 3 4 5 () Die Variante mapM_ verwirft lediglich das Endergebnis, d.h. nur die Seiteneffekt interessiert uns. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen forM Zum Abschluß noch ein (vielleicht) alter Bekannter: forM :: [a] -> (a -> IO b) -> IO [b] forM_ :: [a] -> (a -> IO b) -> IO () Beispiel Man kann flip mapM und anonymen Funktionen fremd-aussehende Programme schreiben: import Control.Monad main = do colors <- forM [1,2,3,4] (\a -> do putStrLn $ "Welche Farbe assoziierst Du mit " ++ show a ++ "?" color <- getLine return color) putStrLn "Farbe der Zahlen 1, 2, 3 und 4 sind: " mapM putStrLn colors Steffen Jost Einführung in die Funktionale Programmierung mit Haskell Funktoren I/O Monadische Funktionen Zusammenfassung Haskell erlaubt I/O im imperativen Stil Unter der Haube bleibt alles rein funktional dank Monaden IO-Aktionen verändern den Zustand der Welt, welche zwischen allen ausgeführten IO-Aktionen herumgereicht wird IO-Aktionen sind Monaden. Alle Monaden sind Funktoren. Steffen Jost Einführung in die Funktionale Programmierung mit Haskell