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.
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.
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.
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.
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.
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.
trace :: String -> a -> a
trace s x = unsafePerformIO ( putStrLn s >> return x )
noOfOpenFiles :: IORef Int
noOfOpenFiles = unsafePerformIO (newIORef 0)
[5]
cast :: a -> b
cast x = let bot = bot
r = unsafePerformIO (newIORef bot)
in unsafePerformIO ( do writeIORef r x
readIORef r )