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
-NotationMehrere 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!
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.
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
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 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.
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 Kommandozeilen-Parameter, 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
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.