Weiter Zurück Inhalt

3. Verschiedene Anwendungen von Monads

Monads erlauben nicht nur die Reihenfolge von Berechnungen festzulegen. Ein Monad kann beliebig viele Informationen enthalten und dem Programm die möglichkeit bieten diese auszulesen oder zu verändern. Desweiteren ist es möglich, nur durch Veränderung der Struktur des Monads, Ausnahmen und Nichtdeterminismus zu implementieren. Mit Hilfe von Monads ist sogar das destruktive Verändern von Daten möglich.

3.1 Status

Durch die Verwendung eines Monads ist genau beschrieben, in welcher Reihenfolge Berechnungen erfolgen sollen. Es ist deshalb möglich, einen Status in einem Monad einzuführen, der in der festgelegten Reihenfolge von Berechnungen gelesen oder verändert werden kann.

Das Monad wird so definiert, daß jede Berechnung des Monads eine Funktion ist, die den Ausgangsstatus in einen Endstatus überführt.

-- a ist der Typ des Status
-- b ist der Typ des Ergebnisses der Berechnung
data State a b = State ( a -> (a, b) )

instance Monad (State a) where
  return x = State (\s -> (s,x) )

  (>>=)  :: (State a) -> ( a -> (State b) ) -> (State b)
  (State f1) >>= f2 
     = State ( \st1 -> let (st2, y)       = f1 st1
                           (State trans ) = f2 y
                       in trans st2 )
Nach dieser Definition des State-Monads wird ein Status von Berechnung zu Berechnung durchgereicht.

Es können jetzt zwei weitere Berechnungen definiert werden, die den Status lesen oder schreiben.

readState    :: State a a
readState    =  State   (\st -> (st, st) )

writeState   :: a -> State a ()
writeState s = State (\_ -> ( s, () ))
Es ist jetzt möglich, z.B. das Beispiel um einen Zähler erweitern, wieviele Multiplikationen stattgefunden haben.

type CountState a = State Int a

tick :: CountState ()
tick = do a <- readState
          writeState (a+1)

extract :: CountState a -> (Int, a)
extract (CountState st) = st 0

mul      :: Int -> Int -> CountState Int
mul x y  =  do tick
               return (x * y)

test = extract . one

> test 5 ==> (2, 1)
In diesem Beispiel liefert test ein Tupel zurück, daß die Anzahl der Aufrufe von mul und das Ergebnis der Berechnung enthält.

Bemerkenswert ist, daß sich die Definitionen von div und one nicht geändert haben, obwohl sie jetzt, im Gegensatz zum Beispiel mit dem Identity-Monad, einen Status durchreichen.

3.2 Ausnahmen

In C wird häufig der Fehlerfall durch einen speziellen Rückgabewert gezeigt. Dies hat den Nachteil, daß der Wertebereich des Ergebnisses beschränkt wird, und nach jedem Funktionsaufruf getestet werden muß, ob der Funktionsaufruf erfolgreich war. In C++, Java und anderen Sprachen wurden Exceptions eingeführt, die einen Fehler darstellen, und nach oben durchgereichten, bis der Fehler behandelt wird. Diese Exceptions können als ein spezieller Rückgabewert gesehen werden, auf den nach jedem Funktionsaufruf geprüft wird.

Die polymorphen Datentypen von Haskell erlauben es, diesen speziellen Fehlertyp zu verwenden, ohne den Wertebereich des Ergebnisses einzuschränken. Die Definition des Monads kann verwendet werden, um die Fehlerübeprüfung nach jedem Aufruf vorzunehmen.

Eine Ausnahmen-Behandlung kann einfach realisiert werden, in dem >>= so definiert wird, daß bei einer fehlgeschlagenen Berechnung, die darauf folgenden Berechnungen nicht mehr berechnet werden, und sofort der Fehler zurückgegeben wird.

data Exception a = Success a | Error String

instance Monad Exception where
  return x = Success x
  fail s   = Error s

  (Success x) >>= f  = f x
  (Error s  ) >>= f  = (Error s)

catch_ :: Exception a -> (Exception a -> Exception a) -> Exception a
catch_ (Success a) _ = (Success a)
catch_ (Error s)   f = f (Error s)

handler :: Num a => Exception a -> Exception a
handler (Error s) = Success 999

div_     :: Int -> Int -> Exception Int
div_ _ 0 =  fail "div 0"
div_ x y =  return (x ´div´ y)

> one 5 ==> (Success 1)
> one 0 ==> (Error "div 0")
> catch_ (one 0) handler ==> 999

In diesem Beispiel wird der Fehler in div_ mit catch_ abgefangen, und von handler bearbeitet. Obwohl sich die Definitionen von mul und one nicht verändert haben, wird in ihnen die Ausnahme korrekt behandelt.

3.3 Nichtdeterminismus

Um Nichtdeterminismus zu implementieren, wird das Exception-Monad verändert. Eine Funktion liefert eine Liste aller erfolgreich berechneten Ergebnisse. Eine fehlgeschlagene Berechnung hat die leere Liste als Ergebnis. Die nachfolgenden Berechnungen werden auf alle Ergebnisse der voherigen Berechnungen angewendet.

data Nd a = Nd [a]

instance Monad Nd where
  return x = Nd [x]
  fail s   = Nd []

  (Nd [] )    >>= f = Nd []
  (Nd (x:xs)) >>= f = let (Nd ys) = f x
                          (Nd zs) = (Nd xs) >>= f in
                          (Nd (ys ++ zs) )


fromTo   :: Int -> Int -> Nd Int
fromTo a b = Nd [a..b]

many     :: Int -> Nd Int
many x   = do y <- fromTo 0 x
              one y

> many 2 ==> [1, 1]
> many 0 ==> []

In diesem Beispiel liefert fromTo eine Liste von Ergebnissen der Funktion. Diese werden jeweils einzeln von one verarbeitet. Die erfolgreichen Berechnungen von one werden gesammelt, und bilden das Ergebnis von many.

Wenn am Ende einer solchen Berechnung nur ein gültiges Ergebnis benötigt wird, werden durch die "lazy evaluation" nur die benötigten Zwischenergebnisse berechet. Durch diese Eigenschaft lassen sich z.B. einfach nichtdeterministische Parser bauen.

3.4 IO

Das Monad IO ähnelt dem Status-Monad. Der Status innerhalb des Monads entspricht der Umgebung des Programmes, also der Welt. Jede Funktion, die IO-Operationen ausführt, verändert diesen Status der "Welt". Das IO-Monad ist ein ADT, es ist also nicht möglich, Teile der Welt aus dem Monad zu extrahieren, und außerhalb des Monads zu verändern.

Alle Berechnungen, die IO-Operationen ausführen, haben als Ergebnistyp das IO-Monad.

-- data IO a = IO(Welt -> (Welt, a))

mul      :: Int -> Int -> IO Int
mul x y  =  do putStrLn( "mul " ++ (show x) ++ " " ++ (show y) )
               return (x * y)

Für das IO-Monad gibt es keine extract-Funktion. Es ist nicht möglich, ein Ergebnis, daß in dem IO-Monad berechnet wurde, zu extrahieren. Alle IO-Operationen eines Programmes sind durch dieses Monad genau vorhersagbar.

unsafePerformIO

Es gibt eine Funktion, die für das IO-Monad wie die extract-Funktion arbeitet. Diese Funktion heißt unsafePerformIO. IO-Operationen die mittels unsafePerformIO ausgeführt werden, sind nicht vorhersagbar. Sie können zu jedem Zeitpunkt der Programmausführung in beliebiger Reihenfolge auftreten.

Die Benutzung von unsafePerformIO sollte, soweit möglich, vermieden werden. Durch unvorsichtige Benutzung ist es möglich, das Typsystem von Haskell auszuhebeln, und Programme zu schreiben, die auf ungültige Speicherbereiche zugreifen.

Es gibt jedoch ein paar Anwendungen, bei denen unsafePerformIO praktisch sein kann.


Weiter Zurück Inhalt