dies & das - haskell

4. Monaten und I/O

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.

4.1 Grundkonzept der Monaden

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 >>= \_ -> k
Es 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 -> e2
Dabei 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.

4.2 Das I/O System von Haskell

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))

4.3 Haskell und imperative Programmierung

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