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