Monade (Informatik)In der funktionalen Programmierung sind Monaden ein abstrakter Datentyp. Historisch wurde der Begriff Monad auch als Name für X in einer mathematischen Gleichung verwendet. Wesentliche Eigenschaft von Monaden ist die Fähigkeit der Übertragung von Werten und Berechnungen eines „einfacheren“ Typs zu Berechnungen eines „höheren“ Typs, der mittels eines Typkonstruktors aus dem einfacheren Typ hervorgeht, sowie die Verknüpfung mehrerer solcher Übertragungen zu einer einzigen. HintergrundDer Hauptnutzen von Monaden ist es, Ein- und Ausgabeoperationen, zustandsbehaftete Berechnungen, Nichtdeterminismus (auch als Iteration über Kollektionen und ihren Kombinationen interpretierbar) und Anderes auszudrücken. Dabei soll die Sprache keine Nebeneffekte einführen.[1] Das Konzept der Monade stammt aus der Kategorientheorie, einem Zweig der Mathematik, welcher mathematische Objekte mittels Morphismen oder Funktoren vergleicht. Die Wörter Monade oder aber auch Funktor sind wiederum von Konzepten in der Philosophie abgeleitet. Die Programmiersprache Haskell ist eine funktionale Sprache, die Monaden stark einsetzt und versucht, monadische Kompositionen zu vereinfachen, beispielsweise durch syntaktischen Zucker (u. a. die sogenannte do-Notation). DefinitionenDie übliche Formulierung einer Monade in der Programmierung hat folgende Komponenten:
Die folgenden Operationen sind typisch für Monaden und können für deren Definition Verwendung finden:
Diese Operationen müssen folgenden Gesetzen gehorchen:
In Anlehnung an HaskellIn Haskell wird eine Monade über die Operationen class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
Die anderen Operationen lassen sich dann über diese beiden definieren: (f >=> g) a = f a >>= g
(fmap f) ma = ma >>= (return . f)
join mma = mma >>= id
Über den Kleisli-OperatorEine Monade lässt sich auch über ihre Kleisli-Kategorie definieren: class Monad m where
return :: a -> m a
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
Die übrigen Operationen ergeben sich dann wie folgt: ma >>= f = (id >=> f) ma
fmap f = id >=> (return . f)
join = id >=> id
Analog zur KategorientheorieIn der Kategorientheorie wird eine Monade üblicherweise über einen Funktor class Monad m where
fmap :: (a -> b) -> m a -> m b
return :: a -> m a
join :: m (m a) -> m a
Die übrigen Operationen lassen sich dann wie folgt realisieren: ma >>= f = (join . (fmap f)) ma
f >=> g = join . (fmap g) . f
Beziehungen zu anderen TypklassenJede Monade ist auch ein Applikativer Funktor und mithin auch ein Funktor. Umgekehrt gilt das nicht. Diese Eigenschaft fand sich aus historischen Gründen nicht explizit in Haskells Standardbibliothek, der Glasgow Haskell Compiler hat dies jedoch mit Version 7.10 eingeführt.[2] Besonders deutlich wird diese Beziehung auch, vergleicht man die kategorientheoretische Definition mit der Funktor-Klasse in Haskell: class Functor f where
fmap :: (a -> b) -> f a -> f b
Dabei muss BeispieleBehälterContainer wie Listen, Mengen, Multimengen stellen Monaden dar, deren Bindeoperation die übergebene Funktion auf alle Elemente anwendet und die dabei erhaltenen Ergebnisse vereinigt. Die Vereinigungsoperation ist dabei jeweils Listenverkettung, Vereinigungsmengenbildung bzw. Bildung der Multimengenvereinigung. Die Einheitsfunktion ergibt Einermengen und -listen. Hier als Beispiel die Monade für verkettete Listen. Das Konzept der Instanz für Listen ist es, eine Liste einzulesen, dann jedes Element an die Funktion zu übergeben und die Ergebnisse zu verbinden. Hier eine Beispielimplementation in Haskell: -- Hier nochmal zur Erinnerung, der Listentyp ist folgendermaßen definiert:
data [a] = [] | a:[a]
-- Als syntaktischer Zucker kann [a,b,c] für a:b:c:[] verwendet werden.
instance Monad [] where
--return :: a -> [a]
return a = [a] -- Per Definition eine Liste mit einem Element zurückgeben
--(>>=) :: [a] -> (a -> [b]) -> [b]
liste >>= f = concat zwischenergebnis where -- Die einzelnen Teillisten zusammenfügen
zwischenergebnis :: [[b]]
zwischenergebnis = map f liste -- Die Funktion auf die Liste abbilden
Vektoren und lineare AbbildungenDer Typkonstruktor bildet hier einen Typ auf einen Vektorraum ab, bei dem als (Namensgeber für eine) Basis dient, und dessen Elemente beispielsweise als Funktionen modelliert werden. Die Bindeoperation hat den Typ . Durch Vertauschen der Argumente erhält man den Typ , an dem man die Semantik erkennen kann: die gegebene Funktion, die auf den Basiselementen definiert ist, wird zu einer vollen linearen Abbildung erweitert. Die Einheitsfunktion bildet das Basiselement (welches in dieser Modellierung noch kein „richtiger“ Vektor ist) auf den entsprechenden Basisvektor ab. State, I/OBei zustandsbehafteten Aktionen dient die Bindeoperation der Verwirklichung der Hintereinanderausführung. Die Einheitsfunktion erstellt eine Aktion, die nichts tut und ein festes Resultat zurückgibt. Das Konzept ist dabei recht natürlich. Wenn man in einer rein funktionalen Programmiersprache einen veränderlichen Status übergeben will, dann macht man das in der Regel auf folgende Weise, hier am Beispiel einer Zählerfunktion: -- Den Zähler hochzählen und den alten Zähler zurückgeben
hochzählen :: Int -> Int -> (Int,Int)
hochzählen schrittweite zählerstand = (zählerstand,neuerZählerstand) where ...
Das Grundprinzip ist, dass man als Parameter den alten Status anhängt und den neuen mit dem Rückgabewert zusammen zurückgibt. Um sich Arbeit zu ersparen, kann man dieses Muster einfach in einen neuen Typen verpacken, der Parameter data Status s a = Status (s -> (a,s))
-- Beispiel:
hochzählen :: Int -> Status Int Int
hochzählen schrittweite = Status $ \zählerstand -> (zählerstand,zählerstand+schrittweite)
Was man jetzt noch braucht, sind ein paar Funktionen, die den Status manipulieren können. Hier zum Beispiel eine Funktion, die den Status auf einen neuen setzt, und eine, die ihn ausliest: setStatus :: s -> Status s ()
setStatus s = Status $ \_ -> ((),s) -- Der alte Status wird ignoriert und durch den neuen ersetzt. Rückgabewert, da unnötig, ().
getStatus :: Status s s
getStatus = Status $ \s -> (s,s) -- Dupliziere den Status in den Rückgabewert.
Dies ist schon fast alles, was nötig ist. Das einzige, was noch fehlt, ist die Möglichkeit mehrere statusverändernde Aktionen zu kombinieren, hier sind Monaden das Werkzeug der Wahl: instance Monad (Status s) where -- Die Typvariable s ist irrelevant für die Definition
--return :: a -> Status s a
return a = Status $ \s -> (a,s) -- Status bleibt unverändert
--(>>=) :: Status s a -> (a -> Status s b) -> Status s b
(Status aktion1) >>= f = Status $ \s -> aktion2 zwischenstatus where -- Status aus aktion1 in aktion2 einspeisen.
(rückgabe1,zwischenstatus) = aktion1 s -- aktion1 ausführen
Status aktion2 = f rückgabe1 -- Rückgabewert aus aktion1 in f einspeisen
Mit diesen Funktionen und dem syntaktischen Zucker der do-Notation (der die monadischen Operationen vor uns versteckt) lässt sich das Beispiel dann folgendermaßen formulieren: hochzählen :: Int -> Status (Int,Int)
hochzählen schrittweite = do zählerstand <- getStatus -- Zählerstand ermitteln
setStatus (zählerstand + schrittweite) -- Zähler setzen
return zählerstand -- alten Zählerstand zurückgeben
-- Hier entzuckert
hochzählen schrittweite = getStatus >>= \zählerstand ->
setStatus (zählerstand + schrittweite) >>= \_ ->
return zählerstand
Andere SprachenLINQ-Abfrageausdrücke in C# sind direkt inspiriert von Haskells Dieselbe Strategie verfolgt Scala im Fall von In der Standardbibliothek von Java 8 sind mindestens zwei Monaden vorhanden, die derselben Namenskonvention gehorchen: die Schnittstellen Weblinks
Monaden in anderen Programmiersprachen
Einzelnachweise
|