Input / Output
Transcription
Input / Output
Input / Output Hello World in Haskell: main :: IO () main = putStrLn "Hello World!" Dieses Programm kann man mit runhaskell ausführen. Die main Funktion muss den Typ IO () haben (dieser kann aber inferiert werden). Sie dient als Startpunkt zur Ausführung des Programms. bash# runhaskell helloworld.hs Hello World! putStrLn erzeugt eine IO-Aktion: ghci> :t putStrLn putStrLn :: String -> IO () Der Ergebnistyp IO () steht für eine IO-Aktion, die ein Ergebnis vom Typ () liefert, wenn sie ausgeführt wird. IO-Aktionen werden ausgeführt, wenn sie Teil des Hauptprogramms (definiert durch main) sind (oder wenn sie in GHCi eingegeben werden). getLine liest eine Zeile von der Standardeingabe: ghci> :t getLine getLine :: IO String IO-Aktion, die einen String liefert, wenn sie ausgeführt wird. do-Notation Mehrere IO-Aktionen können mit do-Notation kombiniert werden. main = do putStrLn "Wie heißt Du?" name <- getLine putStrLn ("Hello " ++ name ++ "!") Ausführen: bash# runhaskell hello.hs Wie heißt Du? World Hello World! 1 Der Linkspfeil holt das Ergebnis aus einer IO-Aktion heraus und bindet es an eine Variable. name hat den Typ String und kann in reinen Funktionen (d.h. solchen ohne IO Typ) verwendet werden. Was ist das Ergebnis von getLine ++ getLine? In einem do-Block können Variablen auch mit einer let-Anweisung gebunden werden. Im Gegensatz zum let-Ausdruck hat die Anweisung kein in sondern die Bindungen sind in den folgenden Anweisungen sichtbar: main = do let name = "World" putStrLn ("Hello " ++ name ++ "!") Man bindet Variablen in do-Blöcken mit let an Ergebnisse von reinen Funktionen und mit dem Linkspfeil an Ergebnisse von IO-Aktionen. Wenn man eine Variable mit let an eine IO-Aktion bindet, ist der Wert der Variablen die IO-Aktion selbst: main = do let gl = getLine a <- gl b <- gl putStrLn (a ++ b) Die IO-Aktion gl kann mehrfach ausgeführt werden und dabei unterschiedliche Ergebnisse liefern. Sie ist eine Abkürzung für die IO-Aktion getLine selbst, nicht für deren Ergebnis. IO-Aktionen können rekursiv definiert werden. Als Beispiel definieren wir unsere eigene getLine Aktion: getLine’ :: IO String getLine’ = do c <- getChar if c == ’\n’ then return "" else do cs <- getLine’ return (c:cs) Die IO-Aktion getChar liefert ein Zeichen von der Standardeingabe. Wir vergleichen dieses Zeichen mit ’\n’ um zu entscheiden, ob wir weiterlesen müssen. In do-Blöcken können wir if-then-else Ausdrücke verwenden, deren then und else Zweige IO-Aktionen (vom gleichen Typ) sind. return :: a -> IO a erzeugt aus einem beliebigen Wert eine IO-Aktion, die diesen Wert zurück liefert. Wir verwenden return um den leeren String zu liefern, wenn das ’\n’-Zeichen gelesen wurde und um im rekursiven Fall die gesamte Zeile aus erstem Zeichen c und restlicher Zeile cs zurück zu liefern. 2 return verhält sich anders als in imperativen Sprachen: main = do a <- return "a" b <- return "b" putStrLn (a++b) return "c" return () Es bricht die Ausführung eines do-Blocks nicht ab sondern verpackt das Argument lediglich in einer IO-Aktion ohne Seiteneffekt. Das obige Programm gibt ab aus und könnte kürzer so geschrieben werden: main = do let a = "a" b = "b" putStrLn (a++b) Da wir das Ergebnis der beiden ersten mit return erzeugten Aktionen sofort wieder mit dem Linkspfeil heraus holen, können wir auch let verwenden. Die Ergebnisse der beiden letzten Aktionen werden nicht verwendet. Wir können die Aktionen also weglassen (da return keinen Seiteneffekt hat). IO-Aktionen können auch (potentiell) unendlich lange laufen. import Data.Char ( toUpper ) main = do c <- getChar putChar (toUpper c) main Dieses Programm liest immer wieder ein Zeichen von der Standardeingabe und gibt es groß aus. Bei Eingabe von hello ergibt sich folgende Ausgabe: bash# runhaskell echo-char.hs hHeElLlLoO Lazy IO Man kann die Standardeingabe in Haskell auch lazy einlesen, d.h. erst wenn sie gebraucht wird. Die IO-Aktion getContents :: IO String liefert die Standardeingabe als lazy String. main = do s <- getContents putStr (map toUpper s) 3 Dieses Programm liest genau wie das obige die Eingabe zeichenweise ein und gibt sie groß wieder aus: ghci> main hHeElLlLoO Obwohl mit map toUpper konzeptuell die gesamte Eingabe auf einmal verarbeitet wird, verarbeitet das Programm die Eingabe zeichenweise: jedes Zeichen wird erst eingelesen, wenn der entsprechende Großbuchstabe ausgegeben werden soll. Die Pufferung der Eingabe wird davon beeinflusst, wie man das Programm ausführt. Im GHCi ist die Pufferung standardmäßig zeichenweise, bei der Ausführung mit runhaskell zeilenweise: bash# runhaskell lazy-echo-char.hs hello HELLO world WORLD Die Art der Pufferung kann man mit Funktionen aus dem System.IO Modul beeinflussen. Das obige Programm verhält sich, als würde es in einer Schleife Zeilen einlesen, ist aber im Gegensatz zum vorher gezeigten Programm nicht rekursiv definiert. Lazy IO wird häufig für Programme verwendet, die die Benutzereingabe zeilenweise verarbeiten, da es erlaubt solche Programme ohne Rekursion zu definieren. Auch der Inhalt von Dateien wird in Haskell lazy eingelesen. Die Funktion readFile :: String -> IO String erwartet als Parameter einen Dateinamen und liefert eine IOAktion, die den Dateiinhalt zurück gibt. Wie bei getContents wird die Datei erst gelesen, wenn der Inhalt von der Berechnung gebraucht wird. Die Funktion writeFile :: String -> String -> IO () nimmt einen Dateinamen und einen String und liefert eine IO-Aktion, die die angegebene Datei mit dem gegebenen String überschreibt. Zum Anhängen eines Strings an eine bestehende Datei, kann man die Funktion appendFile :: String -> String -> IO () verwenden. Variante der Uppercase-Konvertierung mit Dateien: main = do s <- readFile "input.txt" writeFile "output.txt" (map toUpper s) Der Inhalt von input.txt wird erst beim Schreiben in output.txt gelesen. Obwohl die map Funktion konzeptuell die komplette Eingabe konvertiert, ist weder die Eingabe noch die Ausgabe jemals komplett im Speicher. Laziness ermöglicht die Verwendung von Zwischenergebnissen, ohne dass diese komplett erzeugt werden. 4 Programmieren mit IO Statt Haskell-Programme mit runhaskell auszuführen, kann man sie auch kompilieren. Zum Beispiel können wir mit dem Kommando bash# ghc --make helloworld aus der Datei helloworld.hs die Datei helloworld erzeugen und diese dann ausführen. bash# ./helloworld Hello World! Als etwas komplizierteres Beispiel schreiben wir ein Programm, das eine Zahl n vom Benutzer einliest und die ersten n Fakultäten ausgibt: import System ( getArgs ) main = do a:_ <- getArgs printFactorials (read a) return () printFactorials :: Int -> IO Int printFactorials 1 = do print 1 return 1 printFactorials n = do facNm1 <- printFactorials (n-1) let facN = n * facNm1 print facN return facN Die IO-Aktion getArgs :: IO [String] liefert die Liste aller KommandozeilenParameter, deren erstes Element wir mit einem Pattern an die Variable a binden. printFactorials berechnet die Fakultätsfunktion und gibt gleichzeitig alle Zwischenergebnisse aus. Ein Nachteil dieser Implementierung ist die Verzahnung der Berechnung von Fakultäten und deren Ausgabe. Besser ist es die Berechnung und die Ausgabe im Programm voneinander zu trennen: main = do a:_ <- getArgs sequence $ map (print.factorial) [1..read a] 5 return () factorial :: Int -> Int factorial n = product [1..n] Dieses Programm berechnet die auszugebenden Fakultäten mit der Funktion factorial ohne Seiteneffekte und gibt diese dann mit der print Funktion aus. Die Funktion sequence :: [IO a] -> IO [a] nimmt eine Liste von IO-Aktionen als Argument, die wir mit der map Funktion erzeugen. Das Ergebnis von sequence ist eine IO-Aktion, die die gegebenen Aktionen der Reihe nach ausführt und die Ergebnisse der Ausführungen in einer Liste zurück gibt. Wir ignorieren diese Ergebnisse und liefern stattdessen () als Ergebnis von main. Haskell-Programme sollten in der Regel dem Muster des zweiten Programms folgen und • als erstes die Eingabe einlesen, • dann mit einem rein funktionalen Programm ein Ergebnis berechnen und • dieses dann ausgeben oder in eine Datei schreiben. Dadurch wird der imperative Anteil eines Programms auf die Ein- und Ausgabe beschränkt. Das eigentliche Programm bleibt seiteneffektfrei und dadurch einfacher verständlich und besser wartbar. Anders als in imperativen Programmiersprachen sind seiteneffektbehaftete Berechnungen in Haskell sogenannte Bürger erster Klasse. IO-Aktionen können, wie oben gesehen, Argumente und Ergebnisse von Funktionen sein und in Datenstrukturen, zum Beispiel in Listen, stecken ohne ausgeführt zu werden. 6