Fakultät für Informatik Chair for Logic and Verification Monads in Haskell Lukas Fürmetz Seminar Fortgeschrittene Konzepte der funktionalen Programmierung (SS15) Betreuer: Julian Brunner Leitung: Prof. Tobias Nipkow Abgabetermin: 22. April 2015 2 Inhaltsverzeichnis 1 Einleitung 4 2 Grundlagen 2.1 Functor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Applicative . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 3 Monads in Haskell 3.1 do-Notation . . 3.2 Anwendung . . 3.2.1 Maybe . 3.2.2 List . . 3.2.3 State . . 3.2.4 IO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 . 7 . 8 . 8 . 9 . 11 . 12 4 Fazit 13 Literaturverzeichnis 14 3 1 Einleitung “A monad is just a monoid in the category of endofunctors, what’s the problem?” ist ein oft zitierter Satz, wenn es um das Verständnis von Monaden in Haskell oder anderen Programmiersprachen geht. Eine Monade ist ein Typklasse mithilfe derer Operationen auf Instanzen dieser verknüpft werden können. Dieses Konzept wird in der puren funktionalen Programmiersprache Haskell unter anderem dazu verwendet, um Operationen mit Seiteneffekten, wie z.B. Ein- und Ausgabe, verwenden zu können. Im folgendem werden zuerst die grundlegenden Konzepte zum Verständnis von Monaden erklärt, anschließend wird speziell auf die Umsetzung und Verwendung in Haskell eingegangen und zum Schluss werden noch einige wichtige Monaden vorgestellt. 2 Grundlagen In diesem Absatz wird erklärt, was grundsätzlich eine Monade ist. Dabei werden zuerst zwei weitere abstrakte Datenstrukturen vorgestellt, nämlich Functor und Applicative, auf welche Monaden aufbauen. 2.1 Functor “Functors are things that can be mapped over”[1]. Dazu gehören in Haskell zum Beispiel List, Maybe oder auch Either. Diese werden dabei mithilfe von Typklassen umgesetzt, wobei diese nur eine Methode vorschreibt und zwar fmap. Diese ist folgendermaßen definiert: class Functor f where fmap :: (a -> b) -> f a -> f b Das heißt fmap erhält als erstes Argument ein Funktion, die als Parameter einen Wert vom Typ a erhält und einen Wert vom Typ b zurück gibt. f ist ein Typkonstruktorvariable und entspricht in der konkreten Instanzierung dieser Typklasse dem Typkonstruktor des Functors. Dies kann mithilfe einer Container-Analogie veranschaulicht werden. Die Funktion fmap erhält einen Container mit Werten von Typ a, wendet die Funktion (a -> b) auf diese an und gibt anschließend einen Container mit Werten von Typ b zurück. Diese Analogie ist aber nicht universell einsetzbar und trifft auf einige Instanzierungen von Functor nicht zu. 4 Um dies zu veranschaulichen, betrachten wir die Datenstruktur List aus dem Modul Data.List, welche eine Instanz von Functor ist: let xs = [1,2,3,4] fmap (+1) xs = [2,3,4,5] In diesem Fall ist der Container die Liste xs = [1,2,3,4]. Wenn man nun die Funktion (+1) mithilfe von fmap auf diesen Container anwendet, erhält man wiederum eine Liste mit jedem Element um eins inkrementiert. Wichtig bei der Implementation einer Instanz dieser Typklasse ist, dass folgende Gesetze erfüllt sind: fmap id == id fmap (f . g) == fmap f . fmap g Man kann fmap auch anders betrachten, nämlich als Funktion die eine Funktion a -> b erhält und eine Funktion zurück liefert, welche nun einen Functor mit Werten von Typ a nimmt und denselben Functor mit Werten von Typ b als Rückgabewert hat. Dies wird lifting genannt.[1] fmap (+1) :: (Functor f, Num b) => f b -> f b 2.2 Applicative Applicative aus dem Modul Control.Applicative erweitert die Typklasse Functor, um die Fähigkeit mit mehreren Funktoren zu arbeiten. Sie ist folgendermaßen definiert: class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b Mithilfe der Funktion pure kann ein Wert in die Instanz von Applicative verpackt werden. Mithilfe von <*> können Berechnungen aneinander gehängt werden und deren Ergebnisse kombiniert werden. Um besser zu verstehen, wozu Applicative gut ist, hilft es sich die Implementation für den Typ Data.Maybe anschauen: instance Applicative Maybe where pure = Just Nothing <*> _ Just f <*> m = Nothing = fmap f m 5 Die Implementation von pure packt einfach den Wert in den Konstruktor Just. Die Funktion <*> hat zwei verschiedene Abläufe. Falls Nothing als Argument übergeben wird, wird Nothing zurückgegeben, da Nothing keine Funktion erhält. Im Gegensatz dazu extrahieren wir im zweiten Fall eine Funktion aus dem Just Container und wenden sie auf das zweite Argument von <*> an. Durch dieses Konstrukt können Funktionen innerhalb eines Containers auf den Wert eines anderen Containers, desselben Typs angewendet werden: ghci> Just (/) <*> Just 42 <*> Just 3 Just 14.0 Hierbei wird die Funktion / auf den Wert Just 42 angewendet, was wiederum den Wert Just ((/) 42) ergibt. Dieser wird wiederum mithilfe von <*> auf Just 3 angewendet, woraus sich das Ergebnis von Just 14.0 ergibt. Bei der Implementation von Applicative sind ein vier weitere Gesetze zu beachten. 1. identity pure id <*> v = v 2. composition pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 3. homomorphism pure f <*> pure x = pure (f x) 4. interchange u <*> pure y = pure ($ y) <*> u 3 Monads in Haskell Monaden sind wiederum eine Erweiterung von Applicative, um beliebige Berechnungen aneinander zu hängen. Eine Monade in Haskell ist, wie auch Functor und Applicative, über eine Typklasse definiert: class Applicative m => Monad m where (>>=) :: forall a b. m a -> (a -> m b) -> m b return return :: a -> m a = pure 6 Diese Definition besteht aus zwei wichtigen Bestandteilen, nämlich >>= und return. Letzterer ist dasselbe wie pure, wie man der Default-Implementation in der fünften Zeile entnehmen kann. Das heißt return nimmt einen Wert und setzt in mithilfe des Type-Konstruktors m in einen Standard-Kontext der Monade. Die Funktion >>= ist das Essenz einer Monade, sie wird auch „bind“ genannt. Sie nimmt als Argumente einen Container, der den Wert von Typ a beinhaltet und eine Funktion a -> m b, welche wiederum einen Wert vom Typ a nimmt und einen Wert von Typ b in einem Container zurück gibt. Mithilfe von „bind“ können Aktionen auf Monaden verknüpft werden. Hier ein Beispiel anhand des Datentyp Maybe, welcher eine Instanz von Monad ist: Just 1 >>= (\x -> return (x * 8)) >>= (\y -> return (y + 2)) -- evaluates to Just 10 Bei der Implementation einer Monade müssen drei Gesetze beachtet werden. Diese werden auch „Monad Laws“ genannt: 1. return a >>= k = k a 2. m >>= return = m 3. m >>= (x -> k x >>= h) = (m >>= k) >>= h 3.1 do-Notation Um das Verknüpfen von Aktionen auf einer Monade einfacher zu machen, stellt Haskell „syntactic sugar“ für „bind“ zur Verfügung, nämlich die „doNotation“. Mithilfe dieser kann das Beispiel in Sektion 3 folgendermaßen umgeschrieben werden: do x <- Just 1 y <- return (x * 8) return (y + 2) -- evaluates to Just 10 7 3.2 Anwendung Nun stellt sich die Frage, wozu Monaden gut sind. Im folgendem werden einige wichtige Monaden in Haskell vorgestellt, um die Nützlichkeit dieser zu veranschaulichen. 3.2.1 Maybe In Sektion 3 wurde schon auf die Maybe Monade eingegangen. Zum Verständnis dieser hilft es einen Blick auf die Implementation zu werfen: instance Monad Maybe where (Just x) >>= k = k x Nothing >>= _ = Nothing return = Just Falls >>= Nothing als Argument übergeben wird, wird die Funktion k ignoriert und Nothing als Ergebnis zurückgegeben. Im anderem Fall mit Just x wird das Ergebnis von k x zurückgegeben. Eine Anwendung von Maybe ist die Verwendung als Rückgabetyp einer Funktion, die unter Umständen keine Ergebnis zurückliefert. Ein Beispiel für eine solche Funktion ist die Funktion lookup aus dem Modul Data.List: lookup :: Eq a => a -> [(a, b)] -> Maybe b lookup sucht nach einem Schlüssel in einer Assoziations-Liste. Falls sie den Wert findet, gibt sie den assoziierten Wert als Just b zurück, ansonsten Nothing. Um nun die Anwendung dieser Monade zu verstehen, konstruieren wir ein kleines Beispiel mithilfe von lookup. Angenommen man hat zwei AssoziationsListen. Die erste bildet einen Namen auf eine ID ab. Die zweite bildet eine ID auf eine Postleitzahl ab: table1 = [("Foo", 1), ("Bar", 2), ("Baz", 3)] table2 = [(1, 81476), (2, 81399), (4, 91232)] Nun wollen wir eine Funktion getPLZ schreiben, welche einen Namen als Argument erhält und die dazugehörige Postleitzahl über die beiden Listen finden soll. Falls ein Postleitzahl gefunden wird, soll diese zurückgegeben werden, im anderem Fall Nothing. Eine einfache Implementierung schaut folgendermaßen aus: 8 getPLZ :: String -> Maybe Integer getPLZ n = case lookup n table1 of Nothing -> Nothing Just id -> lookup id table2 Mithilfe von Monaden können wir dies vereinfachen: getPLZ :: String -> Maybe Integer getPLZ n = lookup n table1 >>= (\id -> lookup id table2) Dieselbe Funktion kann auch mithilfe der do-Notation umgeschrieben werden: getPLZ :: String -> Maybe Integer getPLZ n = do id <- lookup n table1 lookup id table2 Mithilfe der Maybe-Monade können also Operationen, die vielleicht ein Ergebnis zurückgeben, miteinander verbunden werden. Sobald eine dieser Funktionen kein Ergebnis liefert, liefert die gesamte Operation kein Ergebnis zurück. Die kann man in Abbildung 1 gut sehen. Nützlich ist dies für die Fehlerbehandlung um zu signalisieren, dass ein Ergebnis nicht gefunden wurde. Wenn man anstatt Nothing eine spezifischere Fehlermeldung zurückgeben möchte, kann man auch auf den Typ Either zurückgreifen. Dieser funktioniert ähnlich wie die Maybe-Monade, aber gibt anstatt Nothing einen zweiten Datentyp zurück. Dabei ist zu beachten, dass Either keine Monade ist, sondern nur Either a. Abbildung 1: Visualisierung der Maybe-Monade 3.2.2 List Eine der wichtigsten Datenstrukturen in Haskell ist die Liste, definiert in Data.List. Sie ist auch eine Monade: 9 instance Monad [] where m >>= k = foldr ((++) . k) [] m return x = [x] Die „bind“ Funktion wendet die Funktion k auf jedes Element der Liste an und konkateniert das Ergebnis.return gibt das Element in einer Liste der Größe 1 zurück. Die Maybe-Monade ist nützlich um Funktionen zu verbinden die 0 oder 1 Ergebnis haben. Im Gegensatz dazu ist die List-Monade nützlich für Funktionen die 0 oder mehr Ergebnisse haben. Man kann mit der List-Monade zum Beispiel ein chemisches Experiment simulieren. Dazu definieren einen neuen Datentyp, welcher vier verschiedene Werte annehmen kann, welche vier verschiedenen Substanzen entsprechen: data Substance = Substance1 | Substance2 | Substance3 | Substance4 deriving (Show, Eq) Die Reaktionen zwischen den Substanzen: react :: Substance -> Substance -> [Substance] react Substance1 Substance2 = [Substance4, Substance4] react Substance3 Substance4 = [Substance1, Substance2] react _ _ = undefined Zwei Experimente: firstExperiment = react Substance1 secondExperiment = react Substance3 Zum Schluss simulieren wir das Experiment mithilfe der List-Monade. Dabei starten wir mit Substance2 und wenden das erste Experiment darauf an. Anschließend wenden wir auf das Ergebnis dieser Reaktion wiederum das zweite Experiment an: simulateExperiment :: [Substance] simulateExperiment = do secondResult <- firstExperiment Substance2 thirdResult <- secondExperiment secondResult return thirdResult Das Ausführen von simulateExperiment ergibt: [Substance1,Substance2,Substance1,Substance2] Die Syntax in 3.2.2 erinnert an den Syntax von List-Comprehensions und tatsächlich können wir simulateExperiment auch mithilfe einer List-Comprehension definieren: 10 simulateExperiment :: [Substance] simulateExperiment = [thirdResult | secondResult <- firstExperiment Substance2, thirdResult <- secondExperiment secondResult] Tatsächlich sind List-Comprehensions syntaktischer Zucker für die „do-Notation“ der List-Monade. Auch „Guards“ in List-Comprehensions können in der ListMonade durch die Funktion guard ausgedrückt werden: example1 xs = [ x * x | x <- xs, even x] example2 xs = do x <- xs guard (even x) return (x * x) Hierbei sind example1 und example2 exakt gleich. 3.2.3 State Haskell ist eine pure, funktionale Sprache somit kann keinen Funktion am globalen Zustand etwas ändern. Manche Algorithmen benötigen jedoch einen Zustand, um diesen zu implementieren. Haskell stellt hierfür die State-Monade zur Verfügung, welche in Control.Monad.State mithilfe des Monad-Transformers StateT definiert ist. Hier ein Beispiel, welche die Funktionsweise illustriert. Wir definieren einen Stack und drei Operationen (Pop, Push, Add) auf diesem. In der Funktion runStack benutzen wir nun die get und put Funktionen der State-Monade, um den aktuellen Zustand auszulesen, bzw. zu verändern. Die State-Monade hat zwei Typvariablen, die erste gibt den Typen des Zustandes an, und die zweite den Typ des Rückgabewertes der Evaluation der State-Monade. 11 data Op = Pop | Push Integer | Add type Stack = [Integer] type OpStack = [Op] add :: Stack -> Stack add (x:y:xs) = (x + y) : xs runStack :: OpStack -> State Stack Integer runStack [] = do xs <- get return (head xs) runStack (x:xs) = do stack <- get case x of Pop -> put (tail stack) Push i -> put (i : stack) Add -> put (add stack) runStack xs Mithilfe der Funktion evalState kann eine State-Monade nun evaluiert werden: let initialStack = [2,5,3,8] evalState (runStack [Add, Pop, Pop, Push 5, Add]) initialStack = 13 Die State-Monade kann als Abstraktion über eine Funktion, welcher ein Zustand übergeben wird, und einen Wert und einen neuen Zustand zurück gibt, verstanden werden. 3.2.4 IO Ein weitere sehr wichtige Monade in Haskell ist die IO-Monade, mit welcher, wie der Name schon sagt, I/O ausführt wird, bevor sie einen Wert zurück gibt. Dies klingt widersprüchlich, da Haskell eigentlich keine Seiteneffekte haben kann. Die IO-Monade ist aber als Beschreibung von Aktionen zu sehen, welche dann zur Laufzeit von der Runtime-Umgebung ausgeführt wird. Damit kann die Sprache rein funktional bleiben, während es die Implementierung nicht ist. Die Funktion main dient als Einstiegspunkt für das Programm und hat den Typ IO)). I/O Operationen können nur von Funktionen ausgeführt werden, welche die IO als Typ des Rückgabewertes in ihrer Signatur haben. 12 Als Beispiel für die Funktionalität dieser Monade eine simple Implementierung des Unix-Tools „cat“: import System.Environment main :: IO () main = do args <- getArgs text <- readFile (head args) print text Dieses Programm liest mithilfe von getArgs die Kommandozeilen-Parameter in eine Stringliste. Anschließend wird eine Datei mithilfe von readFile eingelesen und mithilfe von print auf der Standardausgabe ausgegeben. 4 Fazit Monaden sind ein sehr nützliches Konzept, welches in Haskell eine sehr wichtige Rolle spielt. Durch diese kann Zustand und I/O in einer rein funktionalen Sprache abgebildet werden. Auch zur Fehlerbehandlung ist es sehr nützlich, wie man am Beispiel der Maybe-Monade sehen kann. Durch die „do-Notation“ können Operationen auf Monaden in einer effizienten, aber auch übersichtlichen Weise dargestellt werden. Alles in allem können Monaden in Haskell auf vielfältige Art und Weise eingesetzt werden und sind damit ein sehr nützliches Werkzeug. 13 Literatur [1] Miran Lipovaca. Learn you a haskell for great good!: a beginner’s guide. no starch press, 2011. [2] Bryan O’Sullivan, John Goerzen, and Donald Bruce Stewart. Real World Haskell: Code You Can Believe In. O’Reilly Media, Inc., 2008. 14