Input / Output

Werbung
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:
1
bash# runhaskell hello.hs
Wie heißt Du?
World
Hello World!
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)
2
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.
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
3
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)
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 IO-Aktion, 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
4
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.
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
5
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]
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
Herunterladen