IO Monad


... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ...

Übersicht: IO Monad


IO() Notation

Die Notation IO() verweist auf eine Aktion, dessen Auswertung die Durchführung dieser Aktion ist. Dabei ist das Ergebnis das leere Tupel, ein uninteressantes Ergebnis.
Ein Beispiel hierfür wäre:

done :: IO()
done = return() 

done liefert ein monadisches Ergebnis vom Typ IO, das uninteressante leere Tupel. Es signalisiert, dass man "fertig" ist.

Der monadische Typ IO ist ein abstrakter Typ, dessen Implementierung verborgen ist. Die allgemeine Form von IO() ist IO a, in der der Typ a in den Monad IO "eingewickelt" wurde.

[ nach oben ]


Operationen

Ausgabe eines Zeichen

Die Funktion putChar gibt ein Zeichen aus. Sie hat folgende Signatur

putChar :: Char -> IO()

Der übergebene Buchstabe wird ausgeben, das Ergebnis ist uninteressant.

Beispiel:

? putChar '!'
!

Sequentialisieren von Kommandos

Ein Operator zum sequentialisieren wäre (>>). Wenn p und q zwei Funktionen sind, dann wird bei p >> q erst das Kommando p ausgeführt und dann das Kommando q. Der Operator hat folgende Signatur:

(>>) :: IO () -> IO () -> IO 

Beispiele für (>>)

Implementierung der bekannten putStr Funktion mit Hilfe von putChar.

write :: String -> IO()
write [] = done
write (c:cs) = putChar c >> write cs

Mit putChar c >> write cs wird das Zeichen am Anfang der Charakterliste ausgegeben und danach mit einem rekursiven Aufruf der Rest der Liste ausgegeben.

Da wir dieses Munster mit dem rekursiven Aufruf schon kennen, können wir die Implementierung auch mit foldr schreiben:

write = foldr (>>) done . (map putChar)

Der Aufruf von map putChar mit einem String liefert eine Liste von Aktionen zurück:

? map putChar "test"
[<<IO action>>,<<IO action>>,<<IO action>>,<<IO action>>]
Da die Auswertung dieser Aktionen die Durchführung ist, werden diese Aktionen mit (>>) sequentialisiert und die Zeichen ausgegeben.

Als letztes wird nun ein writeln implementiert:

writeln :: String ->IO()
writeln cs = write cs >> putChar '\n'

Zuerst wird der String mit write ausgegeben und danach ein Zeilenumbruch am Ende anhängt.

Ein Zeichen einlesen

Die Funktion getChar liest ein Zeichen ein und gibt es gleichzeitig wieder aus. Die Signatur sieht wie folgt aus:

getChar :: IO Char

Ein einfacher Aufruf dieser Funktion funktioniert aber nicht, da der Rückgabewert kein Char ist, sondern ein IO Char. Der Aufruf sieht folgendermaßen aus:

? getChar
ERROR: Cannot find show function for IO Char

Daraus folgt: Ausdrücke vom Typ IO a mit a ungleich () können nicht ausgegeben werden.

Doch die Kombination mit done ergibt nun dieses Ergebnis:

? getChar >> done
x

Der Sequenzoperator ist aber nur dann geeignet, wenn bei p >> q das Ergebnis von p nicht interessant ist. Deshalb gibt es einen weiteren Operator, den Bind-Operator.

Der Bind-Operator (>>=)

Der Bind-Operator hat folgende Signatur:

(>>=) :: IO a -> ( a -> IO b) -> IO b

Wenn p und q Funktionen sind, dann wird bei p >>= q zuerst p ausgeführt und der Rückgabewert x vom Typ a an q übergeben, also wird q mit x aufgerufen: q x.

Beispiele für Bind-Operator

Ein Zeichen einlesen und sofort wieder ausgeben.

? getChar >>= putChar
xx
Wenn ein Zeichen eingegeben wird, erscheint dieses doppelt, da getChar und putChar jeweils eins ausgeben. Das Zeichen, das von getChar eingelesen wird, wird an putChar übergeben.

Eine weitere Funktion liest n Zeichen ein:

readn :: Int -> IO String
readn 0 = return []
readn (n+1) = getChar >>= q
    where q c = readn n >>= r
	      where r cs = return (c:cs)

Die Funktion getChar übergibt das eingelesene Zeichen an eine Funktion q, die als Parameter einen Charakter bekommt. Die Implementation der Funktion q beinhaltet den rekursiven Aufruf von readn und übergibt den Reststring an eine weitere Funktion r. Diese liefert die Konkatenation des eingelesenen Charakters und des Reststrings zurück.

Ähnlich sieht die Funktion aus, die Charakter bis zur Eingabe eines Zeilenumbruchs einliest.

readln :: IO String
readln = getChar >>= q
    where q c = if c =='\n'
		then return []
		else readln >>= r
		    where r cs = return (c:cs)

Nachdem ein Zeichen eingelesen wurde, muss dieses Überprüft werden, ob es ein Zeilenumbruch ist. Wenn nicht, ruft sich die Funktion wieder selber auf.

Auffallend bei den letzten beiden Funktionen war, dass mit Hilfe von geschachtelten where-clauses die übergebenen Werte durch den Bind-Operator an Namen gebunden wurden. Um dies zu umgehen, kann die do-Notation verwendet werden.

Die do-Notation

Die do-Notation ist eine alternative Schreibweise für den Bind-Operator mit geschachtelten where-clauses. Dies ist also kein neues Konstrukt, sondern nur "syntaktischer Zucker". In dem do-Block werden alle Kommandos sequentiell ausgeführt und das Ergebnis einer Funktion kann mit Hilfe von <- an eine Variable gebunden werden. Wichtig hierbei ist, dass es sich dabei nur um ein single assignment und kein update assignment (wie in imperativen Programmiersprachen) handelt.

Beipiele für die do-Notation

Die folgenden Beispiele sind die gleichen wie zuvor für den Bind-Operator.

n Zeichen einlesen:

readn :: Int -> IO String
readn 0     = return []
readn (n+1) = do c <- getChar
		 cs <- readn n
		 return (c:cs)

Das eingelesene Zeichen durch getChar wird an die Variable c gebunden, der Reststring vom rekursiven Aufruf an cs und die Konkatenation von c und cs wird zurückgeliefert.

Zeichen bis zum Zeilenumbruch einlesen:

readln :: IO String
readln = do c <- getChar
	    if c =='\n'
               then return []
               else do cs <- readln
		       return (c:cs)

Auch hier können die Ergebnisse der Funktionen getChar und readln an Namen gebunden werden.

[ nach oben ]


Zusammenfassung

Die Klassendefinition des allgemeinen Monads m.

class Monad m where
   return :: a -> m a
   (>>=)  :: m a -> (a -> m b) -> m b
   (>>)   :: m a -> m b -> m b

Es handelt sich um ein Monad, wenn die Sequenz-, return- und Bind-Operatoren implementiert werden. Der return-Operator "wickelt" einen Wert in ein Monad m ein.

Der Sequenz-Operator (>>) kann durch den Bind-Operator ausgedrückt werden (Default-Implementation):

p >> q =  p >>= \ _ -> q

Der übergebene Wert von der Funktion p wird nicht beachtet und q aufgerufen.

Also müssen nur noch return und der Bind-Operator definiert werden.


... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ... [ nach oben ] ...