Monaden

  • Marco Block-Berlitz
  • Adrian Neumann
Chapter
Part of the Xpert.press book series (XPERT.PRESS)

Zusammenfassung

Monaden sind ein mathematisches Konzept aus der Kategorientheorie und werden in Haskell unter anderem auch für die Ein- und Ausgabe verwendet.

Viele Haskell-Studenten haben gerade mit dem Monaden-Konzept Schwierigkeiten und einige glauben, dass die Verwendung von Monaden den Prinzipien der funktionalen Programmierung widerspricht.

Monaden sind ein mathematisches Konzept aus der Kategorientheorie und werden in Haskell unter anderem auch für die Ein- und Ausgabe verwendet.

Viele Haskell-Studenten haben gerade mit dem Monaden-Konzept Schwierigkeiten und einige glauben, dass die Verwendung von Monaden den Prinzipien der funktionalen Programmierung widerspricht.

Das ist aber nicht richtig. Monaden bieten einfach eine Möglichkeit an, Funktionen elegant miteinander zu kombinieren, bei denen es aufgrund ihrer Typen zu Problemen bei dem Einsatz der normalen Funktionskomposition kommt (inspiriert wurde dieses Kapitel unter anderem von [17.71]).

17.1 Einführung und Beispiele

Wir werden zunächst einige Beispiele zur Erläuterung geben, damit wir behutsam in das Konzept der Monaden einsteigen können [17.74].

17.1.1 Debug-Ausgaben

Bei der Lösung komplexer Probleme, kann schnell der Punkt erreicht werden, an dem sich ein geschriebenes Programm anders verhält als es erwartet wird. In solchen Situationen wäre es hilfreich, wenn neben den eigentlichen Berechnungen auch noch eine Mitschrift des Programmablaufs erstellt werden könnte. Anschließend kann leichter eine gründliche Fehleranalyse vollzogen werden.

Da Funktionen in Haskell aber keinen globalen Zustand verändern können, in der eine solche Mitschrift gespeichert werden könnte und auch immer nur einen Wert zurückgeben, müssen die Typen der Funktionen zwangsweise geändert werden.

17.1.1.1 Rückgabewert und Funktionskomposition

Nehmen wir an, dass wir die beiden folgenden Funktionen *f und *g entworfen haben: * f :: a -> b g :: b -> c Damit wir eine Mitschrift generieren können, müssen sie neben den ursprünglichen Rückgabewerten noch ein Stückchen Mitschrift liefern. Wir könnten an dieser Stelle die Funktionen zu *f' und *g' erweitern, indem wir beispielsweise Strings hinzufügen und beides als Tupel kombinieren.

Damit ist auch weiterhin gewährleistet, dass wir nur ein Ergebnis zurückliefern: * f' :: a -> (b, String) g' :: b -> (c, String)

Soweit so gut. Jetzt können wir in der Funktion *f' zum Beispiel noch einen String „ *f' aufgerufen“ zurückgeben. Allerdings wird es umständlich sein, die Funktionen *f' und *g' zu komponieren. Wir müssen uns darum kümmern, die Mitschriften zusammenzuführen.

Einfache Funktionskomposition funktioniert nicht mehr, da die Eingabe von *g' nicht mehr zur Ausgabe von *f' passt. Stattdessen müssen wir schreiben: * h x = let (fErg, fMit) = f x (gErg, gMit) = g fErg in (gErg, gMit ++ fMit)

In einem Programm kann diese Situation sehr häufig auftreten und es wäre sehr aufwändig, stets eine neue Kompositions-Konstruktion hinzuschreiben.

17.1.1.2 Eigene Eingabetypen definieren

Alternativ zur Vorgehensweise des vorigen Abschnitts, könnten wir eine Funktion *verbinde schreiben, die den Eingabetypen anpasst und sich um die nötige Verknüpfung der Mitschriften kümmert.

Da die Erstellung der Mitschriften einen sequentiellen Ablauf impliziert, stellen wir das Argument vom Typen *(a, String) an die erste Stelle. Jetzt können wir bei einer Verkettung von Funktionen, diese von links nach rechts lesen: * verbinde :: (a,String) -> (a -> (b, String)) -> (b, String) verbinde (x,s) f = let (fErg, fMit) = f x in (fErg,s++fMit)

Diese Funktion erleichtert uns das Arbeiten und wir können die Komposition komfortabel notieren: * h'' x = f x ‘verbinde‘ g

17.1.1.3 Identitätsfunktion

Oft kann der Einsatz einer Identitätsfunktion nützlich sein. Die normale Identität können wir aber nicht über *verbinde mit unseren Funktionen verknüpfen, da die Typen nicht passen.

Wir nennen unsere neue Funktion *einheit, da sie das neutrale Element bezüglich *verbinde ist, genau wie die normale Identität das neutrale Element für die normale Funktionskomposition ist. Mit der Funktion *einheit können wir eine Funktion *lift definieren, die normale Funktionen kompatibel mit *verbinde macht: * einheit x = (x, "") lift f = einheit . f Mit den drei Funktionen *verbinde, *einheit und *lift können wir jetzt fast genauso komfortabel arbeiten wie zuvor. Hinzu kommt allerdings die Möglichkeit, Debug-Ausgaben machen zu können: * f x = (x, "f aufgerufen. ") g x = (x, "g aufgerufen. ") h x = f x ‘verbinde‘ g ‘verbinde‘ (\x -> (x, "fertig. "))

Das können wir gleich in Hugs ausprobieren: * Hugs> h 5 (5,"f aufgerufen. g aufgerufen.fertig. ")

Wie erwartet werden die Funktionen von links nach rechts ausgewertet.

17.1.2 Zufallszahlen

Ebenfalls problematisch ist der Einsatz von Zufallszahlen. Ihre Verwendung scheint geradezu ein Widerspruch zum Konzept der mathematischen Funktionen zu sein, die nur von ihren Eingaben abhängig sind. Tatsächlich ist es aber so, dass Computer in den meisten Fällen keine echten Zufallszahlen generieren.

Nach einer mathematischen Vorschrift werden sogenannte Pseudozufallszahlen generiert, die nur so aussehen, als wären sie wirklich zufällig [17.25]. Es wird dabei ein Generator eingesetzt, der einen internen Zustand hat und sich nach jeder Abfrage einer Zufallszahl verändert. Dieser verhält sich absolut deterministisch, was zur Folge hat, dass ein Generator im selben Zustand immer dieselbe Zahl liefert.

Haskell bietet zur Erzeugung eines Zufallswerts die in *Data.Random definierte Funktion *random :: StdGen -> (a, StdGen). Normale Funktionen können also Zufallszahlen verwenden, wenn sie noch einen Wert vom Typ *StdGen erhalten. Wie genau Zufallszahlen eingesetzt werden, wollen wir an dieser Stelle nicht betrachten, vielmehr kümmern wir uns darum, die Generatoren zu verwalten.

Die Funktion *f mit *f :: a -> bändern wir dazu um in: * f :: a -> StdGen -> (b, StdGen) Der veränderte Zufallszahlengenerator soll mit dem Ergebnis zurückgeben werden, damit die nächste Funktion diesen wieder verwenden kann, um neue Zufallszahlen zu generieren.

Jetzt ist es aber wieder schwierig, zwei Funktionen zu verknüpfen. Schauen wir uns als Beispiele die Funktionen *f und *g an, die durch die Funktion *h verknüpft werden sollen: * f :: a -> StdGen -> (b, StdGen) g :: b -> StdGen -> (c, StdGen) h :: a -> StdGen -> (c, StdGen) h a gen = let (fErg, fGen) = f a gen in g fErg fGen

Genau wie in Abschn. 17.1.1.2 wollen wir eine Funktion *verbinde schreiben, die die gewöhnliche Funktionskomposition ersetzt.

Wenn wir schon dabei sind, schreiben wir auch gleich noch eine Funktion *lift, die es uns ermöglicht, deterministische Funktionen in eine Kette von nichtdeterministischen Funktionen einzureihen: * verbinde :: (StdGen -> (a, StdGen)) -> (a -> StdGen -> (b, StdGen)) -> StdGen -> (b, StdGen) verbinde g f gen = let (a, gen') = g gen in f a gen' einheit x gen = (x, gen) lift f = einheit.f

17.2 Monaden sind eine Typklasse

Die Beispiele aus den vorhergehenden Abschnitten haben eine gemeinsame Struktur, die sich in einer Typklasse zusammenfassen lässt. Dazu definieren wir einen Typen *Debug für die Debugausgaben und *Random für die Zufallszahlen: * data Debug a = D (a, String) data Random a = R (StdGen -> (a, StdGen)) Die Typklasse, die eine Funktion zum Verbinden und ein neutrales Element bezüglich dieser Funktion beinhaltet, ist in Haskell bereits vordefiniert und heißt *Monad. Neben diesen beiden Funktionen enthält sie die nicht unumstrittene Funktion *fail, die bei Fehlern aufgerufen wird. Im nächsten Abschnitt wird klar, warum wir sie benötigen.

Die Typklasse sieht dann so aus: * infixl 1 >>, >>= class Monad m where – verbinde (>>=) :: m a -> (a -> m b) -> m b – verbinde aber ignoriere das Ergebnis der ersten Funktion (>>) :: m a -> m b -> m b – neutrales Element bzgl. >>= return :: a -> m a fail :: String -> m a m >> k = m >>= \_ -> k fail = error

Wie immer bei Typklassen erklärt die Klassendefinition zwar, welchen Typ die geforderten Funktionen haben müssen, ihre Semantik bleibt aber verborgen und nur dem Programmierer überlassen.

Damit ein Typ sich aber rechtmäßig Monade nennen darf, müssen die Funktionen die drei folgenden Gesetze erfüllen. Die Rechtsidentität

*m >>= return = m

die Linksidentität

*return x >>= f = f x

und die Assoziativität

*(m >>= f) >>= g = m >>= (\x.f x >>= g).

Wir können jetzt beispielsweise unseren Typen für Funktionen, die Zufallszahlen einsetzen, zu einer Instanz dieser Klasse machen: * instance Monad Random where (R m) >>= f = R $ \gen -> (let (a,gen') = m gen (R b) = f a in b gen') return x = R $ \gen -> (x,gen)

Die Definitionen von *>>=und *return sind genauso wie davor *verbinde und *einheit. Wir mussten uns nur noch um den Konstruktor kümmern, was die Notation ein bisschen schwieriger zu verstehen gemacht hat.

17.3 do-Notation

Die ganzen Ein- und Ausgaben in Haskell werden über Monaden gelöst. Da Monaden sehr häufig vorkommen und damit nicht so lange Ketten aus *>>= entstehen, wird mit der *do-Notation eine besondere Syntax angeboten.

Eine Reihe von monadischen Anweisungen wird als *do-Block geschrieben, in dem die Anweisungen in der Regel von oben nach unten abgearbeitet werden. Tatsächlich hängt das allerdings von der verwendeten Monade und der Definition von *>>= darin ab.

17.3.1 Allgemeine Umwandlungsregeln

Im Haskell-Report wird beschrieben, wie die uns bereits bekannte Syntax mit *>>= in die *do-Notation umwandelt werden kann und umgekehrt [17.59].

Es gibt vier Umwandlungsregeln:
  1. 1.

    Einzelne Anweisungen brauchen keine Umformung. Das *do wird einfach weggelassen: * do {e} = e

     
  2. 2.

    Wenn bei der Ausführung einer Anweisung der Rückgabewert nicht verwendet wird, kann diese einfach nach vorne gezogen werden: * do {e; anweisungen} = e >>= \_ -> do {anweisungen}

     
  3. 3.

    Wird der Rückgabewert einer Anweisung mit einem Pattern gebunden, muss eine Hilfsfunktion geschrieben werden, die das Pattern match übernimmt und die restlichen Anweisungen ausführt, oder aber *fail aufruft, wenn das Pattern-match fehlschlägt: * do {pattern <- e; anweisungen} = let ok pattern = do {anweisungen} ok _ = fail "pattern match failure" in e >>= ok

     
  4. 4.

    Wird ein Wert mit *let gespeichert, kann das vor den do-Block gezogen werden. Es ist zu beachten, dass das *in im *do-Block optional ist: * do { let deklarationen in anweisungen} = let deklarationen in do {anweisungen}

     
An dieser Stelle wird auch klar, warum *fail Teil der Typklasse *Monad sein muss. Dadurch, dass Pattern matches in der *do-Syntax erlaubt sind, kann es passieren, dass Fehler auftreten, die irgendwie behandelt werden müssen.

Die Möglichkeit *fail durch eine passende Funktion zu überschreiben, erlaubt es Programmierern, sinnvoll mit solchen Fehlern umzugehen.

Zum Beispiel kann *fail in der *Maybe-Monade *Nothing zurückgeben und so die ganze Berechnung zum Scheitern bringen.

17.3.2 Umwandlungsregeln für if-then-else

Etwas Vorsicht ist bei der Umwandlung von *if-then-else-Strukturen in die *do-Notation geboten. Schauen wir uns den kleinen aber feinen Unterschied an. Die folgende Umwandlung ist falsch:

* f = do anweisung1 if p then anweisung2 anweisung3 else anweisung4 anweisung5

Richtig wäre es, beiden Anweisungsblöcken ein *do voranzusetzen: * f = do anweisung1 if p then do anweisung2 anweisung3 else do anweisung4 anweisung5

Zusätzlich ist zu beachten, in beiden *do-Blöcken denselben Rückgabewert zu haben. Ein *else-Zweig darf auch nie leer gelassen werden, wie das etwa bei imperativen Sprachen erlaubt ist.

17.3.3 Beispiel

Die bisher vorgestellten Umwandlungsregeln wollen wir jetzt für ein größeres Beispiel verwenden.

Die folgende Funktion, die mit do-Notation geschrieben ist, soll nach den Regeln so umgeformt werden, dass nur noch >>= zum Einsatz kommt: * f :: (Num t, Num t1, Monad m) => m (t, t1) g :: (Num a, Monad m) => a -> m Bool h :: (Num t) => t -> t variante1 x = do f b <- g x let y = x + 2 if b then do (a,c) <- f return (a+h x) else return (h y)

Die Funktionen *f, *g und *h geben wir nicht explizit an, da ihre Definition für dieses Beispiel keine Rolle spielt.

In diesem Programm kommen der Reihe nach alle Strukturen einmal vor, für die wir Umformungsregeln kennen. Schritt für Schritt angewendet gelangen wir zu dieser äquivalenten Funktion: * variante2 x = f >>= \_ -> let ok b = let y = x + 2 in if b then let ok2 (a,c) = return (a+h x) ok2 _ = fail "pattern match failure" in f >>= ok2 else return (h y) ok _ = fail "pattern match failure" in g x >>= ok

Da die do-Notation offensichtlich die Lesbarkeit erhöht, werden wir sie von nun an recht häufig verwenden.

17.4 Vordefinierte Monaden

Es gibt in Haskell viele bereits vordefinierte Monaden. In den folgenden Abschnitten werden wir nur eine kleine Auswahl vorstellen. Ausführliche Beschreibungen finden sich beispielsweise hier [17.61]. Dort gibt es auch Beispiele für alle hier vorgestellten Monaden.

17.4.1 Monade Writer

Die Monade *Writer verhält sich im Prinzip ähnlich, wie die zu Beginn definierte Monade *Debug. Allerdings ist *Writer nicht nur auf Strings als Mitschrift beschränkt.

Stattdessen lassen sich alle Typen verwenden, die eine Instanz der Klasse *Monoid sind. Das ist eine Klasse, die die mathematische Struktur Monoid darstellt. Ein Monoid ist eine Menge von Elementen (z.B. Strings), einer assoziativen Operation *mappend, um zwei Elemente der Menge zu einem neuen Element zusammenzuziehen (etwa die *++-Operation) und einem neutralen Element *mempty bezüglich dieser Operation (dem leeren String).

Die Typklasse für *Monoid sieht folglich so aus: * class Monoid a where – das neutrale Element mempty :: a – die assoz. Operation mappend :: a -> a -> a – falls eine effizientere Möglichkeit zum Konkatenieren – von Listen des Typen existiert, kann diese Funktion – überschrieben werden mconcat :: [a] -> a mconcat = foldr mappend mempty

Eine *Monad-Instanz für die Monade *Writer ist praktisch identisch zur Instanz der Monade *Debug (s. Abschn. 17.1.1): * newtype Writer w a = Writer { runWriter :: (a, w) } instance (Monoid w) => Monad (Writer w) where return a = Writer (a, mempty) m >>= k = Writer $ let (a, w) = runWriter m (b, w') = runWriter (k a) in (b, w ‘mappend‘ w')

Um zu unterstreichen, dass die Writer-Monade nur eine allgemeine Version unserer Debug-Monade darstellt, zeigen wir hier noch einmal das Beispiel aus Abschn. 17.1.1.2. Die Definition der Funktionen ändert sich dabei nur leicht: * f :: Int -> Writer String Int f x = Writer (x, "f aufgerufen. ") g x = Writer (x, "g aufgerufen. ") h x = f x >>= g >>= \x -> Writer (x,"fertig. ")

Aber die Ausgabe bleibt dieselbe: * Hugs> runWriter (h 5) (5,"f aufgerufen. g aufgerufen. fertig. ")

Tatsächlich kommt die Writer-Monade typischerweise beim logging und tracing zum Einsatz.

17.4.2 Monade Reader

Die Monade *Reader ist für Berechnungen nützlich, die lesend auf einen gemeinsamen Zustand zugreifen. Beispielsweise müssen die meisten Teile eines CGI-Skripts auf die Umgebungsvariablen des Webservers zugreifen. Die Monade *Reader ist also das Gegenstück zu *Writer.

Die Instanz ist sehr einfach zu schreiben: * newtype Reader r a = Reader { runReader :: r -> a } instance Monad (Reader r) where return a = Reader $ \_ -> a m >>= k = Reader $ \r -> runReader (k (runReader m r)) r

Die *Reader-Monade ist beispielsweise dann nützlich, wenn ein globales Wörterbuch zum Verarbeiten von Eingaben benötigt wird und vermieden werden soll, dieses explizit von Funktion zu Funktion herumreichen zu müssen. Das kommt zum Beispiel vor, wenn wir einen Wert eines eigenen Datentypen aus einer Liste von Strings erstellen wollen.

Im folgenden einfachen Beispiel wollen wir aus einer Liste der Farbinformationen *["Rot","Magenta","Gruen","Cyan"], eine Liste von Werten eines Datentyps erstellen, der verschiedene Rot- und Grün-Nuancen repräsentieren soll: * data Farbe = Rot Int | Gruen Int deriving Show

Dabei wollen wir ein globales Wörterbuch verwenden, um das Programm später leichter erweitern zu können: * type Woerterbuch = [(String,Int)] beispielBuch :: Woerterbuch beispielBuch = [("Cyan", 1), ("Magenta",1), ("Zinnober",2)]

Die Wörter „Rot“ bzw. „Grün“ in der Eingabe künden an, zu welcher Farbe das nächste Wort gehören soll. Wir definieren eine Funktion *liesFarben, die anhand dieser Wörter eine Fallunterscheidung macht und jeweils eine spezielle Funktion zum Übersetzen der Nuancenbezeichnungen aufruft: * liesFarben :: [String] -> Reader Woerterbuch [Farbe] liesFarben [] = return [] liesFarben ("Rot":rest) = do (nuance,rest') <- rot rest erg <- liesFarben rest' return (nuance:erg) liesFarben ("Gruen":rest) = do (nuance,rest') <- gruen rest erg <- liesFarben rest' return (nuance:erg)

Dabei sind die Funktionen *rot und *gruen so definiert: * rot :: [String] -> Reader Woerterbuch (Farbe,[String]) rot (inp:rest) = do buch <- ask let Just (_,farbe) = find (\(k,v) -> k==inp) buch return (Rot farbe,rest) gruen :: [String] -> Reader Woerterbuch (Farbe, [String]) gruen (inp:rest) = do buch <- ask let Just (_,farbe) = find (\(k,v) -> k==inp) buch return (Gruen farbe,rest)

In der Klasse *Control.Monad.Reader gibt es die vordefinierte Funktion *ask, die uns die in der Monade gespeicherte Umgebung liefert. In unserem Beispiel ist es das Wörterbuch.

Da wir unser Beispiel einfach gehalten haben, um die Konzepte zu verdeutlichen, erfordert es ein wenig Phantasie, den Vorteil der Verwendung der Monade zu sehen.

Der Einsatz dieser fortgeschrittenen Methoden lohnt sich erst dann wirklich, wenn viele Funktionen und eine komplizierte globale Umgebung verwaltet werden soll. Ein Beispiel der nötigen Größe würde aber zu weit vom Thema ablenken.

17.4.3 Monade State

Die Monade *State ist die Verknüpfung zwischen den Monaden *Reader und *Writer. Berechnungen in *State können sowohl lesend als auch schreibend auf einen gemeinsamen Zustand zugreifen. Sie ist von der Idee her identisch zur Monade *Random, die wir weiter oben selbst definiert haben.

Allerdings ist der Typ des Zustands nicht wie bei uns auf Zufallszahlengeneratoren beschränkt, sondern kann vom Programmierer frei gewählt werden: * newtype State s a = State { runState :: s -> (a, s) } instance Monad (State s) where return a = State $ \s -> (a, s) m >>= k = State $ \s -> let (a, s') = runState m s in runState (k a) s'

Die *State-Monade ist immer dann nützlich, wenn zustandsbehaftete Systeme simuliert werden sollen. Ein Beispiel für ein solches System ist ein einfacher Getränkeautomat, der Limonade verkauft. Um den Code einfach zu halten, hat unser Automat nur drei Zustände, die in Abb. 17.1 gezeigt werden.
Abb. 17-1

Die Zustände des Getränkeautomaten

Zunächst definieren wir uns einen Datentypen *Automat, der die Zustände des Automaten repräsentiert und verschiedenen Aktionen für die Interaktion mit einem Benutzer bereitstellt: * data Automat = A { zustand :: Int, eingenommen :: Int, guthaben :: Int, limoUebrig :: Int } deriving Show data Aktion = Geld Int | Flaschen Int | Entnehmen

Als nächstes können wir die Überführungsfunktion *automat schreiben, die eine Liste von Aktionen erhält und den Zustand des Automaten Schritt für Schritt verändert. Als Ergebnis erzeugen wir eine Liste von Strings, die die ausgeführten Aktionen wiedergibt.

Wir verwenden die in *Control.Monad.State vordefinierten Funktionen *get und *put, um den Zustand zu manipulieren: * automat :: [Aktion] -> State Automat [String] automat [] = do z <- get return ["Einnahmen: " ++ (show (eingenommen z))] automat ((Geld n):as) = geld n as automat ((Flaschen n):as) = flaschen n as automat (Entnehmen:as) = do z <- get if (zustand z /= 2) then error "keine Flaschen vorhanden" else case (guthaben z) of n | n == 0 -> put (z {zustand = 0}) otherwise -> put (z {zustand = 1}) erg <- automat as return ("Flaschen entnommen":erg)

Die Funktion *geld erhöht das Guthaben und wechselt in den Zustand\( 1 \). Da der Automat kein Rückgeld gibt, wird die eingenommene Summe erhöht: * geld n as = do z <- get put (z {zustand = 1, guthaben = guthaben z + n, eingenommen = eingenommen z + n}) erg <- automat as return ("Geld bezahlt":erg)

Wenn Flaschen verlangt werden, überprüfen wir, ob genug Guthaben und genügend Limonade vorhanden ist: * flaschen n as = do z <- get if (limoUebrig z < n) then error "nicht genug Limo übrig" else case (zustand z) of 0 -> error "kein guthaben" otherwise -> put (z { zustand = 2, guthaben = guthaben z - n, limoUebrig = limoUebrig z - n}) erg <- automat as return ("Flaschen gekauft":erg)

Um uns zu überzeugen, dass unser Automat auch das Richtige tut, testen wir diesen auf der Konsole: * Hugs> runState (automat [Geld 5,Flaschen 2,Entnehmen,Flaschen 3, Entnehmen]) (A {zustand = 0, eingenommen = 0, guthaben = 0, limoUebrig = 5}) (["Geld bezahlt","Flaschen gekauft","Flaschen entnommen", "Flaschen gekauft","Flaschen entnommen","Einnahmen: 5"], A {zustand = 0, eingenommen = 5, guthaben = 0, limoUebrig = 0})

Wie auch schon bei der *Reader-Monade ist dieses Beispiel ein wenig zu klein, um den Vorteil der monadischen Strukturen aufzuzeigen. Es sollte aber genügen, um eine Intuition für die Verwendung zu entwickeln.

17.4.4 Monade List

Die *List-Monade ist für Berechnungen gedacht, die mehr als ein Ergebnis haben können. Zum Beispiel gibt es mehrere Folgezustände beim Durchsuchen eines Graphen. Oft wird auch die Intuition der nicht-deterministischen Funktionen verwendet. Das Programmieren mit *List erinnert stark an den Programmierstil von logikbasierten Sprachen wie beispielsweise Prolog [17.22].

In der *List-Monade ist *>>=als *concatMap implementiert. Wenn wir die *List-Monade einsetzen, versteckt die monadische Notation, dass alle Berechnungen nicht nur auf einem Wert stattfinden und ein Ergebnis geliefert wird.

Jede Funktion wird auf allen Elementen der Arbeitsliste aufgerufen und liefert auch selbst wieder eine Liste von Ergebnissen. Auch *fail hat in dieser Monade eine sinnvolle Bedeutung, da mit der leeren Liste ein passender Wert für „keine Lösung möglich“ vorhanden ist: * instance Monad [ ] where (x:xs) >>= f = f x ++ (xs >>= f) [] >>= f = [] return x = [x] fail s = []

Tatsächlich aber haben wir die Monade *List in der Form der list-comprehensions schon viel früher kennengelernt. Sie sind nur etwas abgewandelte Syntax für die *do-Notation in dieser Monade: * [a | a <- as, p a] == do {a <- as; if (p a) then return a else fail ""}

Mit der *List-Monade können wir sehr leicht einen besonderen Sortieralgorithmus implementieren, den SlowSort. Er trägt den Namen wegen seiner außerordentlich schlechten average-case Laufzeit von \( O(n!) \). SlowSort arbeitet, indem alle Permutationen einer Eingabeliste nacheinander betrachtet werden, bis die sortierte Permutation gefunden wurde [17.29].

Schreiben wir also zunächst eine Funktion, die die Permutationen einer Liste berechnet. Die Permutationen können leicht rekursiv charakterisiert werden. Eine leere Liste hat nur eine Permutation. Die Permutationen einer \( n \)-elementigen Liste können berechnet werden, indem nacheinander jedes Element der Liste entfernt wird und anschließend die Permutationen der \( (n-1) \)-elementigen Restliste berechnet werden.

Mit der *List-Monade lässt sich das so aufschreiben: * perm :: Eq a=>[a] -> [[a]] perm [] = [[]] perm as = do a <- as b <- perm (delete a as) return $ a:b

Um die Permutationen zu filtern, verwenden wir die Funktion *guard aus *Control.Monad. Ebensogut könnte auch ein *if-then-else-Konstrukt verwendet werden.

Da wir im Prinzip alle sortierten Permutationen finden, uns aber nur die erste interessiert, verwenden wir *head. Durch Lazy evaluation wird so auch mit der Berechnung weiterer Permutationen aufgehört, sobald die erste sortierte gefunden wurde. * slowSort :: Ord a => [a] -> [a] slowSort as = head $ do p <- perm as guard (istSortiert p) return p istSortiert as = and $ zipWith (<=) as (tail as)

Bei dieser Implementierung tritt der worst-case auf, wenn die Liste umgekehrt sortiert vorliegt. Probieren wir die Funktion mit einer kleinen Eingabeliste aus: * Hugs> slowsort [3,1,2,7,0,5] [0,1,2,3,5,7]

Sie können ja einmal probieren, längere Listen mit diesem Verfahren zu sortieren.

17.5 Ein- und Ausgaben

Ein- und Ausgaben sind in Haskell problematisch, da diese Funktionen keine mathematischen Funktionen sind. Vielmehr hängt ihr Ergebnis vom Zustand der Welt ab. Zum Beispiel verändert eine Funktion, die den Benutzer um eine Eingabe bittet, den Zustand des Benutzers.

Zunächst wollen wir uns mit den stream-basierten Eingaben unter Haskell beschäftigen und lernen dann im anschließenden Abschnitt die Vorteile von Monaden in diesem Zusammenhang kennen.

17.5.1 Stream-basierte Eingaben

Eine Möglichkeit in Haskell Ein- und Ausgaben zu realisieren, stellt die Stream-Abstraktion dar. Hier wird das Programm als eine Funktion betrachtet, die einen unendlichen Strom von Eingabeaktionen zu einem Strom von Ausgabeaktionen umwandelt. Wieder wird die Lazyness von Haskell verwendet, um dem Benutzer zu ermöglichen, den Eingabestrom nach und nach zu generieren.

Als Typen für die Ein- und Ausgabe wird der *String verwendet. Damit basieren stream-basierte Eingaben unter Haskell auf Funktionen *f mit der Signatur: * f :: String -> String

Die Interaktion mit dem Nutzer geschieht dann durch Einsatz der Funktion *interact :: (String -> String) -> IO (). Setzen wir in *interact die Identitätsfunktion *id ein, erhalten wir ein Programm, das alle Eingaben einfach wieder zurückgibt: * Hugs> interact id echo echo

Dabei wurde das erste *echo vom Benutzer eingegeben und das zweite vom Programm ausgegeben. Wegen der später in Abschn. 17.5.2.3 beschriebenen Pufferung der Eingaben, erfolgt hier erst nach dem Beenden der Zeile eine Ausgabe, obwohl man vielleicht ein zeichenweises Echo erwarten würde.

Damit unser Programm sinnvoll mit der Welt interagieren kann, müssen wir natürlich dafür sorgen, dass die Funktion immer nur einen endlichen Präfix der Eingabe verarbeiten muss, bevor ein Stück Ausgabe produziert werden kann. Die einfachste Art, das zu erreichen, ist, den unendlichen Eingabestrom in handliche Pakete aufzuteilen und diese dann nacheinander zu verarbeiten.

Dazu können wir zum Beispiel die zwei Funktionen *lines und *unlines aus der Prelude verwenden. Die Funktion *lines zerlegt Zeichenketten, die das Trennsymbol *\naufweisen in Teillisten: * Hugs> lines "aa\nbb\nbb" ["aa","bb","bb"]

Die Funktion *unlines führt die Umkehrung aus und beendet die Zeichenkette mit einem *\n: * Hugs> unlines ["aa","b","ccc"] "aa\nb\nccc\n"

Demzufolge gilt: * Hugs> (unlines.lines) "ich\nbin\nein\ntext\n"=="ich\nbin\nein \ntext\n" True

Für eine bessere Übersicht einer sequentiellen Verarbeitungskette werden wir die Funktionskomposition (s. dazu Abschn. 6.7) umdrehen. Wir lesen die auf eine Eingabe angewendeten Funktionen von links nach rechts: * (>.>) :: (a->b) -> (b->c) -> (a->c) g >.> f = f.g

Für das bessere Verständnis definieren wir Eingabe und Ausgabe vom Typ *String: * type Eingabe = String type Ausgabe = String

Jetzt können wir beispielsweise mit dem neuen Operator für die Funktionskomposition eine eingegebene Zeichenkette umdrehen: * beispielUmdrehen :: Eingabe -> Ausgabe beispielUmdrehen = lines >.> map reverse >.> unlines

Dieses Programm sollten Sie auf der Konsole ausprobieren.

17.5.2 Monade IO

Wir haben mit der stream-basierten Eingabe schon einen alternativen Ansatz kennengelernt. Der monadische Ansatz, den wir jetzt verfolgen, verspricht aber mehr Flexibilität.

Wie gesagt, verändern wir bei Ein- und Ausgabe den Zustand der Welt. Wir müssen unsere Funktionen also davon abhängig machen. Zum Glück haben wir mit der Monade *State schon eine Möglichkeit kennengelernt, das Weiterreichen eines Zustands zu organisieren.

Die Monade *IO macht tatsächlich nicht viel anderes als die Monade *State, nur dass der Zustand der Welt weitergereicht wird. Dieses Zustandsobjekt kann nicht vom Programmierer erzeugt werden, sondern wird der *main-Funktion als impliziter Parameter bei der Ausführung des Programms übergeben. Ausnahmen stellen Funktionen dar, deren Namen mit *unsafe beginnen, wie beispielsweise *unsafePerformIO. Das bedeutet, dass es kein *runIO geben kann, diese Aufgabe ist der *main Funktion vorbehalten.

Es ist wichtig zu verstehen, dass Inhalte vom Typ *IO a nicht wirklich Werte sind, so wie Inhalte vom Typ *Random a nicht wirklich Werte waren. Tatsächlich versteckt sich dahinter eine Funktion *RealWorld -> a.

Damit ist die *IO-Monade, die einzige Monade, bei der etwas Hintergrundmagie passiert. Durch unsere Kenntnis der Monade State wissen wir eigentlich schon alles was wir benötigen, um Ein- und Ausgaben in Haskell zu machen. Wir müssen uns lediglich ansehen, welche Funktionen uns bereits vordefiniert vorliegen.

In den folgenden Abschnitten werden wir einige kennenlernen.

17.5.2.1 Bildschirmausgaben

Um Inhalte auf dem Bildschirm auszugeben, gibt es unter anderem die folgenden Funktionen.

Mit *putChar kann ein einzelnes Zeichen auf dem Bildschirm ausgegeben werden. Es wird das leere Tupel, vergleichbar mit *void in anderen Programmiersprachen, zurückgeliefert. * putChar :: Char -> IO ()

Analog zu *putChar kann mit der Funktion *putStr ein ganzer String ausgegeben werden: * putStr :: String -> IO ()

Diese Funktion könnten wir uns mit Hilfe von *putChar auch schnell selbst definieren: * putStr :: String -> IO () putStr = sequence_.map putChar

Außerdem gibt es eine Funktion *putStrLn, die nach dem String noch einen Zeilenumbruch ausgibt. Komfortabel ist die Funktion *print, die jeden Wert automatisch in einen String umwandelt: * print :: Show a => a -> IO ()

17.5.2.2 Tastatureingaben

Passend zu den Ausgabe-Funktionen gibt es auch nützliche Funktionen zur Eingabe. Die Funktion *getChar liest ein einzelnes Zeichen ein. Dabei muss darauf geachtet werden, dass die Eingabepufferung dementsprechend eingestellt ist. * getChar :: IO Char

Was es mit der Eingabepufferung auf sich hat, wird im nächsten Abschnitt noch erläutert. Analog zu *putStr gibt es die Funktion *getLine, die eine Zeile einliest: * getLine :: IO String

Auch diese können wir wieder über *getChar definieren: * getLine :: IO String getLine = do c <- getChar if (c == '\n') then return "" else do cs <- getLine return (c:cs)

Wieder aus Gründen des Komforts, gibt es eine Funktion, die automatisch *read auf dem eingelesenen String ausführt: * readLn :: Read a => IO a

17.5.2.3 Eingabepufferung

Bei Ein- und Ausgaben, die nur ein einzelnes Zeichen betreffen sollen, tritt bei vielen Betriebssystemen das Problem auf, dass erst Enter gedrückt werden muss, bevor *getChar das getippte Zeichen einliest. Oder im Ausgabefall, dass erst ein Zeilenumbruch ausgegeben werden muss, damit *putChar etwas auf dem Bildschirm ausgibt.

Das hängt damit zusammen, dass das Betriebssystem aus Effizienzgründen zu kurze IO-Aktionen vermeidet. Stattdessen werden die Aktionen gepuffert und Blockweise ausgeführt. Das wird auch als Buffering bezeichnet.

Glücklicherweise kann der Programmierer Einfluss auf dieses Verhalten nehmen. Neben den beiden in den vorhergehenden Abschnitten angegebenen Funktionen, gibt es ein alternatives System von Funktionen, die nicht automatisch von der Standardeingabe lesen, sondern ein sogenanntes Handle erhalten. Das ist ein spezieller Wert, der symbolisch für ein *IO-Gerät, eine Datei oder ähnliches steht. Damit die Funktionen, die Handles verwenden, eingesetzt werden können, muss *System.IO importiert werden.

Eine dieser Funktionen ist *hSetBuffering, mit der zwischen zeilen- und blockweiser Pufferung umgeschalten werden, bzw. mit der die Pufferung sogar ganz ausgestellt werden kann: * hSetBuffering:: Handle -> BufferMode -> IO ()

Das Handle für die Standardeingabe ist *stdin, für die Standardausgabe *stdout. Die drei BufferModes sind *NoBuffering, um die Pufferung ganz auszuschalten: * hSetBuffering stdin NoBuffering

*LineBuffering ist die Standardeinstellung für zeilenweise Pufferung: * hSetBuffering stdin LineBuffering

Als dritten Modus haben wir noch das *BlockBuffering, für blockweise Pufferung: * hSetBuffering stdin BlockBuffering

Leider gibt es keine Garantie, dass *hSetBuffering auch wirklich etwas bewirkt. Nur wenn der gewünschte Modus auch unterstützt wird, hat der Aufruf einen Effekt. Mit dem Compiler GHC 6.10.4 ist es aufgrund eines Fehlers nicht möglich, unter Windows die Pufferung für Tastatureingaben auszuschalten [17.62].

Wie gesagt, dient das Buffering der Effizienzsteigerung bei Ein- und Ausgaben. Es sollte nur dann ausgeschaltet werden, wenn nicht das gewünschte Verhalten erzeugt werden kann.

17.5.2.4 Beispiel: Hangman

In diesem Beispiel implementieren wir eine einfache Version des bekannten Hangman-Spiels, bei dem der Spieler ein Wort erraten muss, in dem er nacheinander Buchstaben rät, die dann aufgedeckt werden. * import Data.Char – für toLower import System.IO – für hSetBuffering – das zu erratende Wort und die Anzahl der Versuche wort = "Lambda" maxVersuche = 5 – im String werden die bereits geratenen Buchstaben – gespeichert. Im Int die Zahl der falschen Versuche hangman :: String -> Int -> IO () hangman geraten falsch {- wenn man mehr falsche Versuche hatte, als erlaubt, hat man verloren. Wenn man alle Buchstaben des Wortes erraten hat, hat man gewonnen -} | falsch > maxVersuche = putStrLn "\nverloren" | all (‘elem‘ geraten) (map toLower wort) = putStrLn "\ngewonnen" hangman geraten falsch = do putStrLn "" printWord geraten putStrLn "\nWelcher Buchstabe?" – ein Zeichen einlesen und zu einem Kleinbuchstaben machen c <- getChar >>= (return.toLower) – testen, ob der Buchstabe vorkommt if (c ‘elem‘ (map toLower wort)) then hangman (c:geraten) falsch else do – falls nicht die Anzahl der Fehlversuche erhöhen putStrLn $ "\n" ++ (show (falsch+1)) ++ " falsch!\n" hangman geraten (falsch+1) {- gibt das Wort aus, zeigt aber nur die geratenen Buchstaben an. Noch nicht geratene Buchstaben werden durch ein _ ersetzt. -} printWord :: String -> IO () printWord geraten = mapM_ putZeichen wort where – mapM = sequence.map putZeichen x | toLower x ‘elem‘ geraten = putChar x | otherwise = putChar '_' main = do hSetBuffering stdin NoBuffering hangman "" 0

Dieses Beispiel funktioniert leider nicht korrekt unter Windows mit dem Compiler GHC 6.10.4, da die Zeilenumbrüche, die wegen des *hSetBuffering-Fehlers eingegeben werden müssen, als Fehlversuche gezählt werden.

Sehen wir uns abschließend eine erfolgreiche Raterunde in der Konsole an: * Hugs> main ______ Welcher Buchstabe? L L_____ Welcher Buchstabe? a La___a Welcher Buchstabe? m Lam__a Welcher Buchstabe? b Lamb_a Welcher Buchstabe? d gewonnen

17.5.3 Dateien ein- und auslesen

Analog zu den Tastatureingaben und Bildschirmausgaben, können Dateien ein- und ausgelesen werden. Dafür gibt es unter anderem die Funktionen *readFile und *writeFile, die eine Datei ein- bzw. auslesen können: * readFile :: IO String writeFile :: FilePath -> String -> IO()

*FilePath ist dabei nur ein Synonym für String. Beide Funktionen arbeiten lazy. Anders als in anderen Programmiersprachen, brauchen wir uns nicht darum zu kümmern, die Daten in kleinere Blöcke aufzuteilen. Haskell macht das schon für uns.

Als Beispiel wollen wir uns ein einfaches Programm schreiben, dass Dateien nach Strings durchsuchen kann.

Zunächst müssen wir uns überlegen, wie wir feststellen können, ob ein Wort in einem Text enthalten ist. Das ist ein viel untersuchtes Problem und es gibt zahlreiche effiziente Algorithmen um es zu lösen. Wir benutzen den naiven Algorithmus, da er am einfachsten zu programmieren ist [17.10]. Entweder steht das gesuchte Wort (Länge \( m \)) ganz am Anfang des Textes (Länge \( n \)) oder es kommt vielleicht im Text vor, wenn wir das erste Zeichen weglassen. Das ergibt eine Laufzeit von \( \Theta(n\cdot m) \).

Dann brauchen wir nur noch eine Datei einzulesen und darin zu suchen. Wenn das Wort enthalten ist, wird der Teil des Textes zurückgegeben, der mit dem Wort beginnt. Ansonsten wird *Nothing zurückgegeben.

Hier unser kleines Programm: * isInfix :: String -> String -> Maybe String isInfix [] _ = Nothing isInfix text wort | wort == take (length wort) text = Just text | otherwise = isInfix (tail text) wort main = do putStrLn "Gib den Pfad zur Datei ein: " filepath <- getLine putStrLn "Gib das gesuchte Wort ein: " wort <- getLine content <- readFile filepath let test = isInfix content wort case test of Nothing -> putStrLn "das Wort war nicht enthalten" Just s -> putStrLn $ "das Wort steht an der Stelle\n " ++ (take 100 s)

Verwenden wir nun unser Programm, um in dem folgenden Textabschnitt (ein Auszug aus dem Wikipedia-Artikel zu Bärtierchen [17.67]), der in der Datei *Baertierchen.txt gespeichert ist, nach dem Wort „Strahlung“ zu suchen:

Bärtierchen überstanden sogar zehn Tage im freien All. Per Satellit hatten Forscher aus Deutschland und Schweden mehrere Proben mit Bärtierchen ins All geschickt und während ihres Aufenthalts Strahlung und Kälte im luftleeren Raum ausgesetzt. Nach ihrer Rückkehr fanden die Wissenschaftler selbst unter denjenigen Bärtierchen Überlebende, die den extremsten Bedingungen ausgesetzt waren.“ * Hugs> main Gib den Pfad zur Datei ein ./Baertierchen.txt Gib das gesuchte Wort ein Strahlung das Wort kommt vor an der Stelle Strahlung und Kälte im luftleeren Raum ausgesetzt. Nach ihrer Rückkehr fanden die Wissenschaftler

Intelligentere string matching-Verfahren können den Aufwand vom naiven Ansatz mit \( \Theta(n\cdot m) \) auf \( \Theta(n+m) \) reduzieren, so z.B. der Knuth-Morris-Pratt-Algorithmus [?].

17.6 Übungsaufgaben

Aufgabe 1) Angenommen wir wollen Funktionen schreiben, die manchmal bei ihren Berechnungen scheitern. Zum Beispiel scheitert die Berechnung einer Wurzel aus einer negativen Zahl, oder das Finden eines Eintrags im Telefonbuch, wenn die gesuchte Person nicht existiert. Wir verwenden den *Maybe Datentypen um das auszudrücken. Funktionen, die scheitern können, haben den Typen *f :: a -> Maybe b. Wenn in einer Kette von Berechnungen eine scheitert, wollen wir, dass die ganze Kette scheitert. Schreiben Sie *verbinde und *einheit für Funktionen dieses Typs.   Aufgabe 2) Angenommen wir wollen Funktionen schreiben, die mehrere Ergebnisse haben können. Zum Beispiel hat die \( n \)-te Wurzel im Komplexen \( n \) Lösungen, oder beim Durchmustern eines Graphen mit Grad n können wir \( n \) Folgezustände haben. Wir benutzen Listen um das auszudrücken. Funktionen, die mehrere Ergebnisse haben können, haben den Typen *f :: a -> [b]. In einer Kette von Berechnungen wollen wir, dass die nächste Funktion auf alle Ergebnisse der vorherigen Funktion angewendet wird. *sqrt ... sqrt soll also alle vier vierten Wurzeln liefern. Schreiben Sie *verbinde und *einheit für Funktionen diesen Typs.   Aufgabe 3) Definieren Sie eine geeignete Instanz von Monad für diesen Datentypen *data M a = M a. Beweisen Sie die drei Monadengesetze.   Aufgabe 4) Definieren Sie geeignete Instanzen von Monad für *Debug a, * Maybe a und *data List a = Nil | Cons a (List a)

Aufgabe 5) Zeigen Sie, dass die drei Monadengesetze für Ihre Instanz für *Maybe gelten.   Aufgabe 6) Zeigen Sie, dass für alle Monaden *m eine Funktion *fmap :: (a->b) -> m a -> m b existiert.   Aufgabe 7) Zeigen Sie, dass man jeden Typen *m, für den eine Funktion *return:: a->m a und eine Funktion *join :: m (m a) -> m a existiert, zu einer Instanz der Monadenklasse machen kann.   Aufgabe 8) Wandeln Sie folgenden Ausdruck in einen *do-Block um: * f x y= g x >>= h >> j y   Aufgabe 9) Wandeln Sie folgenden do-Block zurück in eine Verkettung von Funktionen mit *>>= : * f x y = do z <- g x let i = h y j z   Aufgabe 10) Definieren Sie folgenden Funktionen in zwei Varianten: Verwenden Sie sowohl *do-Notation als auch explizite *>>= Aufrufe. *sequenz :: Monad m => [m a] -> m [a] (vordefiniert als *sequence) *monadFold :: Monad m => (a -> b -> m a) -> a -> [b] -> m a (vordefiniert als *foldM)

Copyright information

© Springer Berlin Heidelberg 2011

Authors and Affiliations

  • Marco Block-Berlitz
  • Adrian Neumann
    • 1
  1. 1.Universität des SaarlandesSaarbrückenDeutschland

Personalised recommendations