The world map is based on NASA's blue marble images.
The picture of the Arts Building is from the University of Saskatchewan Archives.
back |
Anders als imperative Sprachen kennen funktionale Sprachen keine Seiteneffekte. Während Funktionen in imperativen Sprachen z.B. globale Variablen manipulieren, auf das Eingabe/Ausgabe-System des Betriebssystems zugreifen oder u.U. sogar wild im Hauptspeicher herumschreiben können, stehen diese Möglichkeiten in funktionalen Sprachen nicht zur Verfügung. Funktionen kommunizieren nur über ihre Parameter und Rückgabewerte. Mehr noch: Da nur mit Definitionen gearbeitet wird, besteht nicht einmal die Möglichkeit, Kommandos auszuführen oder für Operationen eine Reihenfolge festzulegen. Dies ist vor allem dann problematisch, wenn es darum geht, in einem Programm mit der "Außenwelt" zu kommunizieren, d.h. mit dem Betriebssystem (und letztlich dem Anwender) gezielt Informationen auszutauschen.
Es gibt prinzipiell zwei Ansätze, dieses Problem zu lösen. Der erste ist, in die funktionale Sprache eine imperative "Untersprache" zu integrieren, über die dann die Kommunikation erfolgen kann. Der zweite besteht darin, die benötigten Eigenschaften der imperativen Welt im Rahmen des funktionalen Paradigmas nachzubilden. Während die meisten funktionalen Sprachen den ersten Weg gegangen sind, ist in Haskell der zweite Ansatz verwirklicht. Dies geschieht, indem das Konzept der Monade für den Zweck der Eingabe/Ausgabe-Steuerung verwandt wird.
Monaden sind Strukturen auf Typen, auf denen bestimmte Operationen definiert
sind. Besonders wichtig sind hierbei die Operatoren >>=
("bind") und >>
sowie die
Funktion return. Generell sind nur sehr allgemeine Regeln für diese Operationen
festgelegt, die einen vielfältigen Einsatz der Monaden erlauben, aber gleichzeitig die
Interpretation des Konzeptes erschweren. Letztlich geht es hier aber darum, das I/O
System von Haskell und die Sequenzierung von Operationen als eine Anwendung
des Monadenkonzepts zu erkennen. Als Typklassen kann man Monaden so
deklarieren.
class Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a m >> k = m >>= \_ -> kEs ist hilfreich, sich eine konkrete Monade als einen Behälter vorzustellen, der Inhalte unterschiedlichen Typs enthalten kann. Dann kann man die oben deklarierten Optionen so interpretieren:
return
nimmt einen Wert vom Typ a
and packt ihn in eine Monade vom Typ m a
ein. Diese Funktion dient also dazu, etwas in die Welt der Monaden einzuführen. >>=
nimmt den Wert aus der Monade, transformiert ihn anhand einer gegebenen
Funktion und packt das Ergebnis wieder in die Monade ein. Man könnte sagen, es
verursacht eine definierte "Inhalts"- oder "Zustands"-änderung in der Monade. >>
macht im Grunde dasselbe wie >>=
, mit der Besonderheit, dass der vorherige
"Inhalt" der Monade keine Rolle spielt.
Um eine Hauptaufgabe der Monaden, die Sequenzierung, hervorzuheben, gibt es für die beiden Operatoren eine alternative, suggestivere Schreibweise.
do e1; e2 = e1 >> e2 do p <- e1; e2 = e1 >>= \p -> e2Dabei legt die erste Definition nahe, dass
>>
der Hintereinanderauswertung (bzw.
-ausführung) von e1
und e2
dient. Die zweite Definition möchte als Entsprechung
der Wertzuweisung in imperativen Sprachen gesehen werden.
Das I/O System ist in eine Monade mit der Bezeichnung IO
integriert. Einige
nützliche Funktionen im Rahmen dieser Monade sind z.B.
getChar :: IO Char putChar :: Char -> IO () return :: a -> IO a
IO ()
steht dabei für die "leere" Monade, d.h. Funktionen, die keinen relevanten
Rückgabewert liefern, bilden auf diesen Typ ab. Wie zu erwarten fügt getChar
einen
Buchstaben aus der Standardeingabe in die IO
Monade ein, putChar
sendet einen
Buchstaben an die Ausgabe.
Es ist wichtig zu verstehen, dass eine Funktion, um das I/O System nutzen zu
können, selbst einen Ergebnistyp IO a
haben muss. Insbesondere kann z.B. die
add
Funktion niemals mit dem I/O System kommunizieren. Es ist also nicht möglich,
zu Debuggingzwecken Textausgaben über Funktionen zu verteilen.
Als Beispiel, wie man diese Monade nutzen kann, soll die Funktion getLine
dienen,
die eine Zeile (bis zum Newline Code ´\n´
) einliest. if/then/else
ist dabei eine
Standardschreibweise in Haskell, die mit dem Monadenkonzept nichts zu tun hat und
genau so funktioniert, wie man es erwartet.
getLine :: IO String getLine = do c <- getChar if c==´\n´ then return "" else do l <- getLine return (c:l)Es ist zu beachten, dass innerhalb des
else
-Blocks ein neuer do
-Block begonnen
werden muss.
Die IO
Monade beinhaltet eine dem Programmierer verborgene Struktur IOError
,
die mögliche Fehler von I/O Operationen (Dateiende, etc.) protokolliert. Die Abfrage,
welcher Fehler aufgetreten ist, geschieht über vordefinierte Funktionen wie
isEOFError
. Um den bei einer Aktion möglicherweise auftretenden Fehler
behandeln zu können, muss diese Aktion mit einer Fehlerbehandlungsroutine
verknüpft werden. Dies leistet die Funktion catch
.
catch :: IO a -> (IOError->IO a) -> IO a getChar1 = getChar `catch` (\e -> return ´\n´)
getChar1
liefert also im Falle eines Fehlers das Newline Symbol. Es besteht auch
die Möglichkeit, nicht behandelte Fehler an die aufrufende Ebene weiterzugeben.
Dazu dient die ioError
Funktion.
ioError :: IOError -> IO a getChar2 = getChar `catch` (\e -> (if isEofError e then return ´\n´ else ioError e))
Der mit do
Blöcken formulierte I/O Code ähnelt - sicher nicht ungewollt - einem
imperativen Programm. Um dies zu verdeutlichen, ist hier noch einmal die
Funktionsdefinition von getLine angegeben und zum Vergleich dieselbe Funktion in
einer imperativen (Pseudo-)Sprache.
Haskell Code:
getLine = do c <- getChar if c==´\n´ then return "" else do l <- getLine return (c:l)imperativer Code:
String getLine() { c = getChar(); if c==´\n´ then return "" else { l = getLine(); return (c:l); }}"Hat Haskell also einfach das imperative Rad neu erfunden?" (Hudak u.a. (1999), S. 36)
zurück | Inhalt | weiter |