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