Erstens: Der Begriff Monade ist ein wenig nichtssagend, wenn man kein Mathematiker ist. Ein alternativer Begriff ist Berechnungsprogramm die etwas besser beschreibt, wofür sie eigentlich nützlich sind.
Sie sind ein Muster für die Verkettung von Operationen. Es sieht ein wenig aus wie die Methodenverkettung in objektorientierten Sprachen, aber der Mechanismus ist etwas anders.
Das Muster wird vor allem in funktionalen Sprachen verwendet (insbesondere in Haskell, das durchgängig Monaden verwendet), kann aber in jeder Sprache verwendet werden, die Funktionen höherer Ordnung unterstützt (d. h. Funktionen, die andere Funktionen als Argumente annehmen können).
Arrays in JavaScript unterstützen das Muster, so dass wir dies als erstes Beispiel verwenden können.
Das Wesentliche des Musters ist, dass wir einen Typ ( Array
in diesem Fall), die eine Methode hat, die eine Funktion als Argument nimmt. Die übergebene Operation muss eine Instanz desselben Typs zurückgeben (d.h. eine Array
).
Zunächst ein Beispiel für eine Methodenverkettung, die nicht das Monadenmuster verwenden:
[1,2,3].map(x => x + 1)
Das Ergebnis ist [2,3,4]
. Der Code entspricht nicht dem Monadenmuster, da die Funktion, die wir als Argument angeben, eine Zahl und kein Array zurückgibt. Die gleiche Logik in Monadenform würde lauten:
[1,2,3].flatMap(x => [x + 1])
Hier liefern wir eine Operation, die eine Array
so dass es jetzt dem Muster entspricht. Die flatMap
Methode führt die angegebene Funktion für jedes Element im Array aus. Sie erwartet bei jedem Aufruf ein Array als Ergebnis (statt einzelner Werte), fasst aber die resultierende Menge von Arrays zu einem einzigen Array zusammen. Das Endergebnis ist also das gleiche, nämlich das Array [2,3,4]
.
(Das Funktionsargument, das einer Methode wie map
o flatMap
wird in JavaScript oft als "Rückruf" bezeichnet. Ich werde es "Operation" nennen, da es allgemeiner ist).
Wenn wir mehrere Vorgänge (auf herkömmliche Weise) verketten:
[1,2,3].map(a => a + 1).filter(b => b != 3)
Ergebnisse im Array [2,4]
Die gleiche Verkettung in Monadenform:
[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])
Ergibt das gleiche Ergebnis, das Array [2,4]
.
Sie werden sofort feststellen, dass die Monadenform um einiges hässlicher ist als die Nicht-Monadenform! Das zeigt nur, dass Monaden nicht unbedingt "gut" sind. Sie sind ein Muster, das manchmal vorteilhaft ist und manchmal nicht.
Beachten Sie, dass das Monadenmuster auf andere Weise kombiniert werden kann:
[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))
Hier ist die Bindung verschachtelt und nicht verkettet, aber das Ergebnis ist das gleiche. Dies ist eine wichtige Eigenschaft von Monaden, wie wir später sehen werden. Sie bedeutet, dass zwei kombinierte Operationen wie eine einzige Operation behandelt werden können.
Die Operation darf ein Array mit verschiedenen Elementtypen zurückgeben, z. B. die Umwandlung eines Arrays mit Zahlen in ein Array mit Strings oder etwas anderes, solange es ein Array bleibt.
Dies lässt sich mit Hilfe der Typescript-Notation etwas formeller beschreiben. Ein Array hat den Typ Array<T>
, donde T
ist der Typ der Elemente im Array. Die Methode flatMap()
nimmt ein Funktionsargument vom Typ T => Array<U>
und gibt eine Array<U>
.
Verallgemeinert ist eine Monade ein beliebiger Typ Foo<Bar>
die eine "bind"-Methode hat, die ein Funktionsargument vom Typ Bar => Foo<Baz>
und gibt eine Foo<Baz>
.
Diese Antworten was Monaden sind. Der Rest dieser Antwort wird versuchen, anhand von Beispielen zu erklären, warum Monaden ein nützliches Muster in einer Sprache wie Haskell sein können, die eine gute Unterstützung dafür bietet.
Haskell und Do-Notation
Um das map/filter-Beispiel direkt nach Haskell zu übersetzen, ersetzen wir flatMap
mit dem >>=
Betreiber:
[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b]
El >>=
Operator ist die Bindungsfunktion in Haskell. Sie tut dasselbe wie flatMap
in JavaScript, wenn der Operand eine Liste ist, aber es ist überladen mit unterschiedlicher Bedeutung für andere Typen.
Aber Haskell hat auch eine eigene Syntax für Monadenausdrücke, die do
-Block, der den Bindungsoperator vollständig ausblendet:
do a <- [1,2,3]
b <- [a+1]
if b == 3 then [] else [b]
Dadurch wird das "Klempnerhandwerk" ausgeblendet, und Sie können sich auf die tatsächlichen Vorgänge konzentrieren, die bei jedem Schritt angewendet werden.
In einem do
-Block, jede Zeile ist eine Operation. Es gilt nach wie vor die Einschränkung, dass alle Operationen im Block den gleichen Typ zurückgeben müssen. Da der erste Ausdruck eine Liste ist, müssen die anderen Operationen ebenfalls eine Liste zurückgeben.
Der Rückenpfeil <-
sieht täuschend echt wie eine Zuweisung aus, aber beachten Sie, dass dies der Parameter ist, der in der Bindung übergeben wird. Wenn also der Ausdruck auf der rechten Seite eine Liste von Ganzzahlen ist, wird die Variable auf der linken Seite eine einzelne Ganzzahl sein - aber für jede Ganzzahl in der Liste ausgeführt werden.
Beispiel: Sichere Navigation (der Typ Maybe)
Genug von Listen, sehen wir uns an, wie das Monadenmuster für andere Typen nützlich sein kann.
Einige Funktionen geben möglicherweise nicht immer einen gültigen Wert zurück. In Haskell wird dies durch das Maybe
-type, das eine Option ist, die entweder Just value
o Nothing
.
Die Verkettung von Operationen, die immer einen gültigen Wert zurückgeben, ist natürlich einfach:
streetName = getStreetName (getAddress (getUser 17))
Was aber, wenn eine der Funktionen Folgendes zurückgeben könnte Nothing
? Wir müssen jedes Ergebnis einzeln prüfen und den Wert nur an die nächste Funktion weitergeben, wenn er nicht Nothing
:
case getUser 17 of
Nothing -> Nothing
Just user ->
case getAddress user of
Nothing -> Nothing
Just address ->
getStreetName address
Ziemlich viele sich wiederholende Kontrollen! Stellen Sie sich vor, die Kette wäre länger. Haskell löst das Problem mit dem Monadenmuster für Maybe
:
do
user <- getUser 17
addr <- getAddress user
getStreetName addr
この do
-Block ruft die Bindungsfunktion für die Maybe
Typ (da das Ergebnis des ersten Ausdrucks ein Maybe
). Die bind-Funktion führt die folgende Operation nur aus, wenn der Wert Just value
, andernfalls übergibt es einfach die Nothing
entlang.
Hier wird das Monaden-Muster verwendet, um sich wiederholenden Code zu vermeiden. Dies ist vergleichbar mit der Verwendung von Makros in anderen Sprachen zur Vereinfachung der Syntax, obwohl Makros das gleiche Ziel auf ganz andere Weise erreichen.
Beachten Sie, dass es sich um die Kombination des Monadenmusters und der monadenfreundlichen Syntax in Haskell, die zu einem saubereren Code führen. In einer Sprache wie JavaScript ohne spezielle Syntaxunterstützung für Monaden würde das Monadenmuster den Code in diesem Fall wohl kaum vereinfachen können.
Veränderlicher Zustand
Haskell unterstützt keine veränderlichen Zustände. Alle Variablen sind Konstanten und alle Werte unveränderlich. Aber die State
kann verwendet werden, um eine Programmierung mit veränderlichem Zustand zu emulieren:
add2 :: State Integer Integer
add2 = do
-- add 1 to state
x <- get
put (x + 1)
-- increment in another way
modify (+1)
-- return state
get
evalState add2 7
=> 9
El add2
baut eine Monadenkette auf, die dann mit 7 als Ausgangszustand ausgewertet wird.
Offensichtlich ist dies etwas, das nur in Haskell Sinn macht. Andere Sprachen unterstützen veränderbare Zustände von Haus aus. Haskell ist im Allgemeinen eine "Opt-in"-Sprache - man aktiviert den veränderlichen Zustand, wenn man ihn braucht, und das Typsystem stellt sicher, dass der Effekt explizit ist. IO ist ein weiteres Beispiel dafür.
IO
El IO
wird für die Verkettung und Ausführung von "unreinen" Funktionen verwendet.
Wie jede andere praktische Sprache hat auch Haskell eine Reihe von eingebauten Funktionen, die eine Schnittstelle zur Außenwelt bilden: putStrLine
, readLine
und so weiter. Diese Funktionen werden als "unrein" bezeichnet, weil sie entweder Seiteneffekte verursachen oder nicht-deterministische Ergebnisse haben. Selbst so etwas Einfaches wie das Abrufen der Uhrzeit gilt als unsauber, weil das Ergebnis nicht deterministisch ist - ein zweimaliger Aufruf mit denselben Argumenten kann unterschiedliche Werte ergeben.
Eine reine Funktion ist deterministisch - ihr Ergebnis hängt nur von den übergebenen Argumenten ab und hat neben der Rückgabe eines Wertes keine weiteren Auswirkungen auf die Umgebung.
Haskell fördert in hohem Maße die Verwendung von reinen Funktionen - dies ist ein Hauptargument für die Sprache. Leider brauchen Puristen einige unreine Funktionen, um etwas Sinnvolles zu tun. Der Haskell-Kompromiss besteht darin, reine und unreine Funktionen sauber zu trennen und zu garantieren, dass reine Funktionen auf keinen Fall unreine Funktionen ausführen können, weder direkt noch indirekt.
Dies wird dadurch gewährleistet, dass alle unreinen Funktionen den IO
Typ. Der Einstiegspunkt in ein Haskell-Programm ist die main
Funktion, die die IO
Typ, so dass wir unreine Funktionen auf der obersten Ebene ausführen können.
Aber wie verhindert die Sprache, dass reine Funktionen unreine Funktionen ausführen? Das liegt an der "faulen" Natur von Haskell. Eine Funktion wird nur ausgeführt, wenn ihre Ausgabe von einer anderen Funktion verbraucht wird. Aber es gibt keine Möglichkeit, eine Funktion zu konsumieren IO
Wert, außer um ihn zuzuweisen main
. Wenn also eine Funktion eine unreine Funktion ausführen will, muss sie mit main
und haben die IO
Typ.
Die Verwendung der Monadenverkettung für IO-Operationen stellt auch sicher, dass sie in einer linearen und vorhersehbaren Reihenfolge ausgeführt werden, genau wie Anweisungen in einer imperativen Sprache.
Dies bringt uns zu dem ersten Programm, das die meisten Leute in Haskell schreiben werden:
main :: IO ()
main = do
putStrLn ”Hello World”
El do
Schlüsselwort ist überflüssig, wenn es nur einen einzigen Vorgang gibt und daher nichts zu binden ist, aber ich behalte es trotzdem aus Gründen der Konsistenz.
El ()
Typ bedeutet "nichtig". Dieser spezielle Rückgabetyp ist nur für IO-Funktionen nützlich, die wegen ihrer Nebenwirkung aufgerufen werden.
Ein längeres Beispiel:
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn "hello" ++ name
Dies führt zu einer Kette von IO
Operationen, und da sie dem main
Funktion, werden sie ausgeführt.
Vergleich von IO
con Maybe
zeigt die Vielseitigkeit des Monadenmusters. Für Maybe
wird das Muster verwendet, um sich wiederholenden Code zu vermeiden, indem die bedingte Logik in die Bindungsfunktion verlagert wird. Für IO
wird das Muster verwendet, um sicherzustellen, dass alle Operationen des IO
Typ sequenziert sind und dass IO
Operationen können nicht zu reinen Funktionen "durchsickern".
Resümee
Meiner subjektiven Meinung nach lohnt sich das Monadenmuster nur in einer Sprache, die eine eingebaute Unterstützung für das Muster bietet. Andernfalls führt es nur zu übermäßig verworrenem Code. Aber Haskell (und einige andere Sprachen) haben eine eingebaute Unterstützung, die die lästigen Teile versteckt, und dann kann das Muster für eine Vielzahl von nützlichen Dingen verwendet werden. Zum Beispiel:
- Vermeidung von sich wiederholendem Code (
Maybe
)
- Hinzufügen von Sprachmerkmalen wie veränderbare Zustände oder Ausnahmen für abgegrenzte Bereiche des Programms.
- Eklige Dinge von schönen Dingen trennen (
IO
)
- Eingebettete domänenspezifische Sprachen (
Parser
)
- Hinzufügen von GOTO in die Sprache.
13 Stimmen
Eric Lippert schrieb eine Antwort auf diese Fragen ( stackoverflow.com/questions/2704652/ ), die aufgrund einiger Probleme auf einer separaten Seite zu finden ist.
74 Stimmen
Hier ist eine neue Einführung in die Verwendung von Javascript - ich fand sie sehr lesenswert.
7 Stimmen
Siehe auch Verschiedene Arten, eine Monade zu sehen .
27 Stimmen
Siehe auch Monaden in Bildern
3 Stimmen
Eine Monade ist eine Reihe von Funktionen mit Hilfsoperationen. Siehe diese Antwort
0 Stimmen
Ein weiterer schneller Artikel, der der Antwort von sigfpe ähnelt: github.com/quchen/articles/blob/master/
0 Stimmen
Siehe meinen Artikel über FPComplete, in dem ich erkläre, WARUM man Monaden verwenden sollte (nicht wie sie funktionieren): bit.ly/MH9zRD
2 Stimmen
Die beste Erklärung, die ich bisher gehört habe, stammt von wikipedia: "Monaden sind programmierbare Semikolons."
0 Stimmen
Ich bin auf diesen Artikel gestoßen: stephanboyer.com/post/9/monads-part-1-a-design-pattern . Ich fand es ist die beste und effizienteste Konzeptvermittlung für Laien wie mich bisher. Der Autor hat eigentlich auch andere Artikel für Monaden.
0 Stimmen
Eine äußerst leicht verständliche Erklärung von Douglas Crockford: youtube.com/watch?v=dkZFtimgAcM
0 Stimmen
Monade ist kein Haskell-spezifisches Konzept, @HelderPereira. Ihr Retag scheint mir falsch zu sein.
1 Stimmen
@Palec Ich weiß, aber in der Frage wird Haskell erwähnt, und die am häufigsten gewählte Antwort verwendet Haskell, um sie zu erklären. Ich dachte nur, es wäre nützlich, um es für Leute, die Haskell lernen, einfacher zu machen, es zu finden, da dies ein sehr wichtiges Konzept der Sprache ist. Es steht Ihnen jedoch frei, es zu entfernen, wenn Sie denken, dass es keinen Sinn macht.
0 Stimmen
Ich möchte sagen, dass Monade ist eine Konstruktion, die Sie alle Ihre Mist in (tun tatsächlichen Job, aka Nebenwirkungen), und präsentieren Sie eine schicke Box, um Ihren Code functionnal (lesen Sie Nebeneffekt frei).
0 Stimmen
Monad ist EDSL. Siehe este : "Irgendwann bemerkte jemand: "Oh, um unreine Effekte aus reinem Code zu erhalten, muss ich Metaprogrammierung betreiben, was bedeutet, dass einer meiner Typen 'Programme, die ein X berechnen' sein muss. Ich möchte ein 'Programm, das ein X berechnet' und eine Funktion, die ein X nimmt und das nächste Programm erzeugt, ein 'Programm, das ein Y berechnet', nehmen und sie irgendwie zu einem 'Programm, das ein Y berechnet' zusammenfügen" (das ist die Bindungsoperation). Die IO-Monade war geboren."
0 Stimmen
Dieser Artikel von Tomasz Nurkiewicz ist die beste Erklärung, die ich für Java-Entwickler gefunden habe.
0 Stimmen
Kürzer: Monade ist (eingebettetes) Interpreter-Muster (wobei Anweisungen der zu interpretierenden Sprache erstklassige Werte unserer Programmiersprache selbst sind).
0 Stimmen
Monaden sind eingebettete domänenspezifische Sprachen, die Anweisungen (Semikolons) enthalten. Die Semikolons können Anweisungen in traditionellen prozeduralen Sprachen, Joins in SQL oder was immer Sie wollen darstellen. Da man Monaden zusammenführen kann, um Sprachen mit einer reicheren Semantik zu bilden, oder Monaden aufteilen kann, kann man Monaden auch als die SEMANTIK der Sprachen selbst betrachten.
2 Stimmen
Leider ist eine Monade eine Sache, die nur von Leuten erklärt werden kann, die nicht wissen, wie man etwas erklärt.