Monads in Haskell - Chair for Logic and Verification

Werbung
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
Herunterladen