Ein Monad ist ein Typ auf den die Operationen bind und return
definiert sind.
In Haskell wird für den Operator bind der Operator >>=
verwendet.
Der Operator >>=
wird verwendet, um 2 Berechnungen aneinander zu binden, und eine
neue, zusammengesetzte, Berechnung zu formen. Das Ergebnis der 1. Berechnung
wird der 2. Berechnung als Parameter übergeben.
return
fügt das Ergebnis einer Berechnung in das Monad ein.
Jede Berechnung die in einem Monad verwendet wird, liefert ihr Ergebnis in dem Monad verpackt zurück.
Die Definition der Klasse Monad
in der Hakell Prelude ist
class Monad m where
(>>=) :: m a -> ( a -> m b ) -> m b
return :: a -> m a
(>>) :: m a -> m b -> m b
fail :: String -> m a
m >> k = m >>= \_ -> k
fail s = error s
Die Operatoren >>
und fail
sind für die Definition eines Monads nicht
notwendig, können aber praktisch sein. Der Operator >>
bindet, genau wie
>>=
zwei Berechnungen aneinander, verwirft aber das Ergebnis der ersten
Berechnung.
fail
kann verwendet werden, um das Fehlschlagen einer Berechnung zu signalisieren.
In der Definition aus der Prelude ruft fail
die Haskell-Funktion error
auf und
führt somit zum Abbruch des Programms. Es ist aber möglich, wie z.B. bei dem Exception-Monad,
die fail
-Funktion neu zu definieren, um das Fehlschlagen der Berechnung an einer anderen
Stelle des Programmes behandeln zu können.
return
ist das Nullelement der Operation >>=
, und >>=
ist assoziativ:
return a >>= k = k a
m >>= return = m
m >>= (\x -> k x >>= h) = (m >>= k) >>= h
Die Signatur einer Funktion, die innerhalb des Monads arbeitet, ist (Monad a) => a b
.
Das Ergebnis der Berechnung wird in das Monad verpackt.
hello :: String -> IO ()
hello x = putStr ("Hello " ++ x)
Für einig Monads ist es noch sinnvoll, eine Funktion extract zu definieren. Diese
Funktion extrahiert das Ergebnis einer Berechnung aus dem Monad, damit das Ergebnis in
funktionalen Programmteilen benutzt werden kann, ohne das Monad zu verwenden.
Die Funktion extract darf aber nur definiert werden, wenn alle Berechnungen innerhalb
des Monads ineinander abgeschlossen sind, und somit als Gesamtheit gesehen funktional sind.
Ein Beispiel für eine solche Berechnung ist ein Programmteil, der einen internen Status
verwaltet, der am Anfang der Berechnung initialisiert wird, und nach der Berechnung nicht
mehr gebraucht wird.
Es ist nicht möglich, die Funktion extract für Berechnungen mit externen Seiteneffekten
zu definieren.
Das einfachste Monad ist das Identity-Monad. Die verwendung dieses Monads hat keine besonderen Auswirkungen. Berechnungen werden wie sonst auch in Haskell durchgeführt. Es kann jedoch trotzdem sinnvoll sein, dieses Monad zu verwenden, um das Monad später um weitere Eigenschaften zu erweitern.
Das Identity-Monad ist definiert durch
data Id a = Id a
instance Monad Id where
(Id x) >>= f = f x
return x = Id x
(Id _) >> f = f
fail s = error s
extract :: Id a -> a
extract (Id x) = x
Ein Beispiel für eine Berechnung mit dem Identity-Monad ist:
mul :: Int -> Int -> Id Int
mul x y = return (x * y)
div_ :: Int -> Int -> Id Int
div_ _ 0 = fail "div 0"
div_ x y = return (x ´div´ y)
one :: Int -> Id Int
one x = mul x x >>= \_ ->
mul x 1 >>= \y ->
div_ y x
> one 5 ==> 1
> one 0 ==> Fehler des Interpreters: "div 0"
Die Berechnungen in diesem Beispiel lassen sich durch einfaches Anwenden
der Operatoren auf one x = (x * 1) / x
reduzieren. Später werden wir jedoch
weitere Monads vorstellen, die das Verhalten verändern,
so dass ganz andere Effekte entstehen.
do
-Notation
Um dem Programmierer das schreiben von den vielen Lambda-Funktionen abzunehmen,
gibt es in Haskell das do
-Konstrukt.
Es erlaubt eine einfachere und kürzere Schreibweise von Berechnungen mit
Monads.
Diese beiden Funktionsdefinitionen sind identisch.
one x = mul x x >> one x = do mul x x
mul x 1 >>= \y -> y <- mul x 1
div_ y x div_ y x
Es ist möglich Zwischenergebnisse mit let
an einen Namen zu binden.
one x = do mul x x
let eins = 1 in
y <- mul x eins
div_ y x
Die do
-Notation verändert die Ausdruckskraft der Sprache nicht, weil
sich Ausdrücke in der do
-Notation sehr einfach in Ausdrücke mit >>=,
und return
umwandeln lassen.
Programmstücke, die in der do
-Notation geschrieben sind, sehen
imperativen Programmen in normalen, imperativen Programmiersprachen sehr
ähnlich.