Zustandsmonaden

Werbung
Zustandsmonaden
Wir betrachten noch einmal den Datentyp für beschriftete Binärbäume
data Tree a = Empty | Node (Tree a) a (Tree a)
und wollen eine Funktion definieren, die die Knoten so eines Baums von links nach rechts
durchnummeriert.
numberTree :: Tree a -> Tree (Int,a)
Beispiel (verkürzt):
ghci> numberTree (N (N E ’a’ E) ’b’ (N E ’c’ E))
N (N E (1,’a’) E) (2,’b’) (N E (3,’c’) E)
Zur rekursiven Definition dieser Funktion über die Struktur von Binärbäumen benötigen
wir einen zusätzlichen Parameter, der angibt, welche die nächste verfügbare Nummer ist.
Wir könnten daher versuchen, die Funktion wie folgt zu definieren
numberTree t = numberTreeWithNum t 1
wobei numberTreeWithNum den Typ Tree a -> Int -> Tree (Int,a) hat. Beim Versuch, den rekursiven Fall von numberTreeWithNum zu definieren, bekommen wir aber das
Problem, dass wir die Größe des linken Teilbaums kennen müssen um die nächste freie
Nummer für den rechten zu berechnen. Statt den linken Teilbaum zweimal zu durchlaufen (einmal zur Nummerierung und einmal zur Berechnung der Größe) ist es besser,
wenn unsere Hilfsfunktion nicht nur den nummerierten Baum sondern auch die nächste
freie Nummer zurückgibt. Die nächste freie Nummer wird dadurch zu einer Art Zustand,
der durch die Berechnung durchgereicht wird.
numberTreeWithState
:: Tree a -> Int -> (Tree (Int,a), Int)
Mit dieser Funktion können wir numberTree definieren und müssen nur das erste Element
des Ergebnisses selektieren, also die letzte freie Nummer ignorieren.
numberTree t = fst (numberTreeWithState t 1)
Unsere Hilfsfunktion definieren wir wie folgt. Einen leeren Baum braucht man nicht zu
nummerieren und die nächste freie Nummer bleibt unverändert:
numberTreeWithState Empty n = (Empty,n)
Bei einem inneren Knoten nummerieren wir erst den linken Teilbaum, ergänzen die
Beschriftung durch die dann nächste freie Nummer und nummerieren dann den rechten
Teilbaum mit einer um 1 größeren Nummer.
1
numberTreeWithState (Node l x r) n =
let (l’,n1) = numberTreeWithState l n
(r’,n2) = numberTreeWithState r (n1+1)
in (Node l’ (n1,x) r’, n2)
Definitionen wie diese sind fehleranfällig, da man Variablen wie n1 und n2 leicht verwechseln kann. Das manuelle Auspacken und Weiterreichen des Zustands wird bei größeren
Programmen schnell unübersichtlich.
Die sequentielle Struktur dieses Programms (erst den linken Teilbaum nummerieren,
dann den rechten), wirft die Frage auf, ob wir es nicht eleganter mit do-Notation implementieren können. Es folgt eine wünschenswerte monadische Variante unserer Hilfsfunktion, die einen Zustand mit Hilfe von Funktionen get und put manipuliert.
numberTreeState Empty = return Empty
numberTreeState (Node l x r) =
do l’ <- numberTreeState l
n <- get
put (n+1)
r’ <- numberTreeState r
return (Node l’ n r’)
Können wir eine Monade definieren, die diese Definition erlaubt? Angenommen
numberTreeState soll denselben Typ haben wie numberTreeWithState, dann müsste
die return-Funktion den Typ a -> Int -> (a,Int) haben. Den Typkonstruktor
IntState für die zugehörige Monade müssten wir dann so definieren:
type IntState a = Int -> (a,Int)
Der bind Operator hätte den Typ IntState a -> (a -> IntState b) -> IntState
b. Außerdem haben wir Funktionen get und put verwendet, die in dem Fall folgende
Typen haben müssten:
get :: IntState Int
put :: Int -> IntState ()
Zur Implementierung dieser und der monadischen Funktionen ist der konkrete Typ des
Zustands (hier Int) unerheblich, wir können von diesem also abstrahieren. Außerdem
definieren wir statt eines Typsynonyms einen neuen Typ mit newtype um Berechnungen in Zustandsmonaden von anderen Funktionen zu unterscheiden, die zufällig einen
passenden Typ haben.
newtype State s a = State (s -> (a,s))
Zu diesem Typ definieren wir eine Funktion
runState :: State s a -> s -> (a,s)
runState (State f) = f
2
mit der man Berechnungen in Zustandsmonaden ausführen kann. Es gilt offensichtlich
runState (State f) = f und für alle a :: State s a auch State (runState a) =
a.
Wir geben nun eine Monad-Instanz für den Typkonstruktor State s an. Die returnFunktion lässt den Zustand unverändert und der bind-Operator reicht ihn durch sein
erstes Argument in die Berechnung des zweiten.
instance Monad (State s) where
return x = State (\s -> (x,s))
a >>= f = State (\s -> let (x,s’) = runState a s
in runState (f x) s’)
Wir müssen nun zeigen, dass diese Implementierung die Monadengesetze erfüllt. return
ist eine Links-Identität für bind:
return x >>= f
= State (\s ->
let (x’,s’) = runState (return x) s
in runState (f x’) s’)
= State (\s ->
let (x’,s’) = runState (State (\s -> (x,s))) s
in runState (f x’) s’)
= State (\s ->
let (x’,s’) = (\s -> (x,s)) s
in runState (f x’) s’)
= State (\s ->
let (x’,s’) = (x,s)
in runState (f x’) s’)
= State (\s -> runState (f x) s)
= State (runState (f x))
= f x
return ist auch eine Rechts-Identität für bind:
a >>= return
= State (\s -> let
in
= State (\s -> let
in
= State (\s -> let
in
= State (\s -> let
(x,s’) = runState a
runState (return x)
(x,s’) = runState a
runState (State (\s
(x,s’) = runState a
(\s -> (x,s)) s’)
(x,s’) = runState a
3
s
s’)
s
-> (x,s))) s’)
s
s in (x,s’))
= State (\s -> runState a s)
= State (runState a)
= a
Schließlich zeigen wir noch das Assoziativgesetz für den bind Operator.
(a >>= f) >>= g
= State (\s -> let (x,s’) = runState (a >>= f) s
in runState (g x) s’)
= State (\s ->
let (x,s’) = runState (State (\t ->
let (y,t’) = runState a t
in runState (f y) t’)) s
in runState (g x) s’)
= State (\s ->
let (x,s’) = (\t -> let (y,t’) = runState a t
in runState (f y) t’) s
in runState (g x) s’)
= State (\s ->
let (x,s’) = let (y,t’) = runState a s
in runState (f y) t’
in runState (g x) s’)
= State (\s ->
let (y,t’) = runState a s
(x,s’) = runState (f y) t’
in runState (g x) s’)
= State (\s ->
let (y,t’) = runState a s
in let (x,s’) = runState (f y) t’
in runState (g x) s’)
= State (\s ->
let (y,t’) = runState a s
in (\t -> let (x,s’) = runState (f y) t
in runState (g x) s’) t’)
= State (\s ->
let (y,t’) = runState a s
in runState (State (\t ->
let (x,s’) = runState (f y) t
in runState (g x) s’)) t’)
= State (\s -> let (y,t’) = runState a s
in runState (f y >>= g) t’)
= a >>= \x -> f x >>= g
4
Es fehlen noch die Definitionen für get und put. Die get-Funktion lässt den Zustand
unverändert und gibt ihn zusätzlich als erstes Argument des Ergebnispaares zurück.
get :: State s s
get = State (\s -> (s,s))
Die put-Funktion ignoriert den durchgereichten Zustand und ersetzt ihn durch den
übergebenen.
put :: s -> State s ()
put s = State (\_ -> ((),s))
Mit diesen Definitionen können wir die Funktion numberTree nun unter Verwendung
der monadischen Hilfsfunktion numberTreeState definieren:
numberTree :: Tree a -> Tree (Int,a)
numberTree t = fst (runState (numberTreeState t) 1)
Es stellt sich die Frage, ob die gezeigte Implementierung die einzig mögliche einer Zustandsmonade ist. Analog zur Verallgemeinerung der Listenmonade durch die MonadPlus
Typklasse können wir die State s-Monade zu beliebigen Zustandsmonaden abstrahieren, indem wir die Schnittstelle in einer Typklasse spezifizieren.
Zustandsmonaden stellen neben den monadischen Operationen zwei Funktionen get und
put zur Verfügung, die wir wie folgt in einer Typklasse abstrahieren können:
class Monad m => MonadState s m where
get :: m s
put :: s -> m ()
MonadState ist eine sogenannte Multi-Parameter-Typklasse, denn sowohl der Zustandstyp als auch der Monaden-Typkonstruktor sind Parameter von MonadState.
Multi-Parameter-Klassen gehören nicht zum Haskell’98 Standard, können aber im GHC
oder GHCi durch die Spracherweiterung MultiParamTypeClasses aktiviert werden.
Um entsprechende Instanzen deklarieren zu können ist zusätzlich noch die Erweiterung
FlexibleInstances notwendig.
Wir können den Typkonstruktor State s zu einer Instanz der Klasse MonadState s
machen, indem wir die vorherigen Defnitionen von get und put in die Instanzdeklaration
schreiben.
instance MonadState s (State s) where
get
= State (\s -> (s,s))
put s = State (\_ -> ((),s))
Wie bei MonadPlus können wir uns auch bei MonadState fragen, welche Gesetze für
Zustandsmonaden erfüllt sein sollen. Zwei sinnvolle Gesetze sind zum Beispiel das Gesetz
get >>= put
=
return ()
5
welches besagt, dass das Setzen des Zustands auf den aktuellen Zustand keinen Effekt
hat und das Gesetz
put s >> get
=
put s >> return s
welches besagt, dass get den zuvor gesetzten Zustand liefert und diesen nicht verändert.
Das folgende zeigt, dass unsere Implementierung diese Gesetze erfüllt.
get >>= put
= State (\s ->
let (x,s’) = runState get s
in runState (put x) s’
= State (\s ->
let (x,s’) = runState (State (\s -> (s,s))) s
in runState (put x) s’)
= State (\s ->
let (x,s’) = (s,s)
in runState (put x) s’)
= State (\s -> runState (put s) s)
= State (\s -> runState (State (\_ -> ((),s))) s)
= State (\s -> ((),s))
= return ()
Auch das zweite Gesetz gilt:
put s
= State
let
in
= State
let
in
= State
let
in
= State
= State
= State
let
in
= State
let
in
= put s
>> get
(\t ->
(x,t’) = runState (put s) t
runState get t’)
(\t ->
(x,t’) = runState (State (\_ -> ((),s))) t
runState get t’)
(\t ->
(x,t’) = ((),s)
runState (State (\s -> (s,s))) t’)
(\t -> (s,s))
(\t -> let (x,t’) = ((),s) in (s,t’))
(\t ->
(x,t’) = runState (State (\_ -> ((),s))) t
runState (State (\s’ -> (s,s’))) t’
(\t ->
(x,t’) = runState (put s) t
runState (return s) t’
>> return s
6
Durch die MonadState-Klasse kann die numberTreeState-Funktion in beliebigen Zustandsmonaden ausgeführt werden, denn sie hat den Typ
numberTreeState
:: MonadState Int m => Tree a -> m (Tree (Int,a))
Bisher kennen wir keine anderen Zustandsmonaden, wir werden aber später alternative
Implementierungen kennen lernen.
7
Herunterladen