Weiter Zurück Inhalt

2. Struktur von Monads

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.

2.1 Das Identity-Monad

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.

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


Weiter Zurück Inhalt