wake-up-neo.com

Monade im Klartext? (Für den OOP Programmierer ohne FP Hintergrund)

In Begriffen, die ein OOP Programmierer verstehen würde (ohne einen funktionalen Programmierhintergrund), was ist eine Monade?

Welches Problem löst es und an welchen Orten wird es am häufigsten verwendet?

EDIT:

Um zu verdeutlichen, nach welcher Art von Verständnis ich gesucht habe, nehmen wir an, Sie würden eine FP) - Anwendung mit Monaden in eine OOP) - Anwendung umwandeln. Was würden Sie tun? Portiere die Verantwortlichkeiten der Monaden auf die OOP app?

676
user65663

UPDATE: Diese Frage war Gegenstand einer immens langen Blog-Reihe, die Sie unter Monaden lesen können - vielen Dank für die tolle Frage!

In Begriffen, die ein OOP) Programmierer verstehen würde (ohne einen funktionalen Programmierhintergrund), was ist eine Monade?

Eine Monade ist ein "Verstärker" von Typen, der bestimmte Regeln einhält und der bestimmte Operationen bereitstellt.

Erstens, was ist ein "Verstärker der Typen"? Damit meine ich ein System, mit dem Sie einen Typ in einen spezielleren Typ umwandeln können. Betrachten Sie beispielsweise in C # Nullable<T>. Dies ist ein Verstärker der Typen. Sie können einen Typ, beispielsweise int, nehmen und diesem Typ eine neue Funktion hinzufügen, nämlich, dass er jetzt null sein kann, wenn dies vorher nicht möglich war.

Als zweites Beispiel betrachten wir IEnumerable<T>. Es ist ein Verstärker von Typen. Sie können einen Typ, beispielsweise string, nehmen und diesem Typ eine neue Funktion hinzufügen, nämlich, dass Sie jetzt eine Folge von Zeichenfolgen aus einer beliebigen Anzahl von einzelnen Zeichenfolgen erstellen können.

Was sind die "bestimmten Regeln"? Kurz gesagt, dass es eine sinnvolle Möglichkeit gibt, Funktionen des zugrunde liegenden Typs auf den verstärkten Typ anzuwenden, sodass sie den normalen Regeln der funktionalen Zusammensetzung folgen. Wenn Sie beispielsweise eine Funktion für ganze Zahlen haben, sagen Sie

int M(int x) { return x + N(x * 2); }

dann kann die entsprechende Funktion auf Nullable<int> bewirken, dass alle Operatoren und Aufrufe dort "auf die gleiche Weise" wie zuvor zusammenarbeiten.

(Das ist unglaublich vage und ungenau. Sie haben nach einer Erklärung gefragt, die nichts über Kenntnisse der funktionalen Zusammensetzung voraussetzt.)

Was sind die "Operationen"?

  1. Es gibt eine "Einheit" -Operation (manchmal verwirrenderweise als "Rückgabe" -Operation bezeichnet), die einen Wert von einem einfachen Typ entnimmt und den äquivalenten monadischen Wert erzeugt. Dies bietet im Wesentlichen die Möglichkeit, einen Wert eines unverstärkten Typs in einen Wert des verstärkten Typs umzuwandeln. Es könnte als Konstruktor in einer OO Sprache implementiert werden.

  2. Es gibt eine "Bind" -Operation, die einen monadischen Wert und eine Funktion, die den Wert transformieren kann, akzeptiert und einen neuen monadischen Wert zurückgibt. Bind ist die Schlüsseloperation, die die Semantik der Monade definiert. Hiermit können Sie Operationen für den unverstärkten Typ in Operationen für den verstärkten Typ umwandeln, die den oben genannten Regeln für die funktionale Zusammensetzung entsprechen.

  3. Es gibt oft eine Möglichkeit, den unverstärkten Typ wieder aus dem verstärkten Typ herauszuholen. Genau genommen ist für diese Operation keine Monade erforderlich. (Obwohl es notwendig ist, wenn Sie eine comonad haben möchten. Wir werden diese in diesem Artikel nicht weiter betrachten.)

Nehmen Sie wieder Nullable<T> Als Beispiel. Sie können ein int mit dem Konstruktor in ein Nullable<int> Verwandeln. Der C # -Compiler kümmert sich um das meiste nullbare "Heben" für Sie, aber wenn dies nicht der Fall ist, ist die Transformation für das Heben einfach: eine Operation, sagen wir,

int M(int x) { whatever }

verwandelt sich in

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

Und das Zurücksetzen eines Nullable<int> In ein int erfolgt mit der Eigenschaft Value .

Es ist die Funktionsumwandlung, die das Schlüsselbit ist. Beachten Sie, wie die tatsächliche Semantik der nullwertfähigen Operation - dass eine Operation auf einem null den null weitergibt - in der Umwandlung erfasst wird. Wir können das verallgemeinern.

Angenommen, Sie haben eine Funktion von int bis int, wie unser ursprüngliches M. Sie können das leicht zu einer Funktion machen, die ein int annimmt und ein Nullable<int> Zurückgibt, da Sie das Ergebnis einfach über den Konstruktor nullable ausführen können. Angenommen, Sie haben diese Methode höherer Ordnung:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

Sehen Sie, was Sie damit machen können? Bei jeder Methode, die ein int annimmt und ein int zurückgibt oder ein int annimmt und ein Nullable<int> Zurückgibt, kann jetzt die nullbare Semantik angewendet werden dazu.

Außerdem: Angenommen, Sie haben zwei Methoden

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

und du willst sie komponieren:

Nullable<int> Z(int s) { return X(Y(s)); }

Das heißt, Z ist die Zusammensetzung von X und Y. Dies ist jedoch nicht möglich, da X ein int annimmt und Y einen Nullable<int> Zurückgibt. Aber da Sie die "Bind" -Operation haben, können Sie diese Arbeit machen:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

Die Bindeoperation für eine Monade bewirkt, dass die Komposition von Funktionen für verstärkte Typen funktioniert. Die "Regeln", die ich oben beschrieben habe, lauten, dass die Monade die Regeln beibehält normaler Funktionszusammensetzung; dass das Komponieren mit Identitätsfunktionen zur ursprünglichen Funktion führt, dass die Komposition assoziativ ist und so weiter.

In C # heißt "Bind" "SelectMany". Schauen Sie sich an, wie es auf der Sequenz-Monade funktioniert. Wir müssen zwei Dinge haben: Einen Wert in eine Sequenz verwandeln und Operationen an Sequenzen binden. Als Bonus haben wir auch "eine Sequenz wieder in einen Wert verwandeln". Diese Operationen sind:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

Die Nullable-Monad-Regel lautete: "Kombinieren Sie zwei Funktionen, die Nullables erzeugen, und prüfen Sie, ob die innere Null ergibt. Wenn dies der Fall ist, erzeugen Sie Null, wenn dies nicht der Fall ist, rufen Sie die äußere mit dem Ergebnis auf." Das ist die gewünschte Semantik von nullable.

Die Sequenz-Monaden-Regel lautet: "Kombinieren Sie zwei Funktionen, die Sequenzen erzeugen, wenden Sie die äußere Funktion auf jedes Element an, das von der inneren Funktion erzeugt wird, und verketten Sie dann alle resultierenden Sequenzen miteinander." Die grundlegende Semantik der Monaden wird mit den Methoden Bind/SelectMany erfasst. Dies ist die Methode, die dir sagt, was die Monade wirklich bedeutet .

Wir können es noch besser machen. Angenommen, Sie haben eine Folge von Ints und eine Methode, die Ints akzeptiert und zu Folgen von Strings führt. Wir könnten die Bindungsoperation verallgemeinern, um die Komposition von Funktionen zu ermöglichen, die verschiedene verstärkte Typen annehmen und zurückgeben, solange die Eingaben einer mit den Ausgaben der anderen übereinstimmen:

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

Jetzt können wir also sagen: "Verstärken Sie diese Reihe von einzelnen Ganzzahlen in eine Folge von Ganzzahlen. Transformieren Sie diese bestimmte Ganzzahl in eine Reihe von Zeichenfolgen, verstärkt in eine Folge von Zeichenfolgen. Fügen Sie nun beide Operationen zusammen: Verstärken Sie diese Reihe von Ganzzahlen in die Verkettung von alle Folgen von Zeichenfolgen. " Mit Monaden können Sie komponieren Ihre Verstärkungen.

Welches Problem löst es und an welchen Orten wird es am häufigsten verwendet?

Das ist eher so, als würde man fragen "Welche Probleme löst das Singleton-Muster?", Aber ich werde es versuchen.

Monaden werden normalerweise verwendet, um Probleme zu lösen wie:

  • Ich muss neue Funktionen für diesen Typ erstellen und weiterhin alte Funktionen für diesen Typ kombinieren, um die neuen Funktionen zu verwenden.
  • Ich muss eine Reihe von Operationen für Typen erfassen und diese Operationen als zusammensetzbare Objekte darstellen, indem ich immer größere Kompositionen aufbaue, bis ich genau die richtige Serie von Operationen dargestellt habe, und dann muss ich anfangen, Ergebnisse aus der Sache zu ziehen
  • Ich muss nebenwirkende Operationen sauber in einer Sprache darstellen, die Nebenwirkungen hasst

In C # werden Monaden verwendet. Wie bereits erwähnt, ist das nullable-Muster der "vielleicht Monade" sehr ähnlich. LINQ ist vollständig aus Monaden aufgebaut. Die SelectMany Methode ist die semantische Arbeit der Komposition von Operationen. (Erik Meijer weist gern darauf hin, dass jede LINQ-Funktion tatsächlich durch SelectMany implementiert werden könnte; alles andere ist nur eine Annehmlichkeit.)

Nehmen wir an, Sie würden eine FP) - Anwendung mit Monaden in eine OOP) - Anwendung umwandeln, um zu verdeutlichen, nach welcher Art von Verständnis ich gesucht habe. Was würden Sie tun? Portieren Sie die Verantwortlichkeiten der Monaden in die OOP app?

Die meisten OOP Sprachen verfügen nicht über ein ausreichend umfangreiches Typensystem, um das Monadenmuster selbst direkt darzustellen. Sie benötigen ein Typensystem, das Typen unterstützt, die höhere Typen als generische Typen sind. Ich würde es also nicht versuchen Ich würde eher generische Typen implementieren, die jede Monade darstellen, und Methoden implementieren, die die drei Operationen darstellen, die Sie benötigen: Einen Wert in einen verstärkten Wert umwandeln, (vielleicht) einen verstärkten Wert in einen Wert umwandeln und eine Funktion umwandeln auf unverstärkten Werten in eine Funktion auf verstärkten Werten.

Ein guter Anfang ist, wie wir LINQ in C # implementiert haben. Studieren Sie die SelectMany -Methode; Dies ist der Schlüssel zum Verständnis der Funktionsweise der Sequenzmonade in C #. Es ist eine sehr einfache Methode, aber sehr mächtig!


Vorgeschlagen, weiterlesen:

  1. Für eine eingehendere und theoretisch fundierte Erklärung der Monaden in C # empfehle ich den Artikel meines Kollegen Wes Dyer ( Eric Lippert ) zu diesem Thema. Dieser Artikel hat mir Monaden erklärt, als sie endlich für mich "geklickt" haben.
  2. Ein gutes Beispiel dafür, warum Sie eine Monade um sich haben möchten (verwendet Haskell in seinen Beispielen).
  3. Art, "Übersetzung" des vorherigen Artikels in JavaScript.

685
Eric Lippert

Warum brauchen wir Monaden?

  1. Wir wollen nur mit Funktionen programmieren. ("funktionale Programmierung" immerhin -FP).
  2. Dann haben wir ein erstes großes Problem. Dies ist ein Programm:

    f(x) = 2 * x

    g(x,y) = x / y

    Wie können wir sagen was soll zuerst ausgeführt werden? Wie können wir eine geordnete Folge von Funktionen bilden (d. H. ein Programm), wobei wir nicht mehr als Funktionen verwenden?

    Lösung: Funktionen erstellen. Wenn Sie zuerst g und dann f möchten, schreiben Sie einfach f(g(x,y)). OK aber ...

  3. Weitere Probleme: Einige Funktionen könnten fehlschlagen (d. H. g(2,0), dividieren durch 0). Wir haben keine "Ausnahmen" in FP. Wie lösen wir das?

    Lösung: Lassen Sie uns Funktionen zwei Arten von Dingen zurückgeben: Anstatt g : Real,Real -> Real (Funktion von zwei Real in einen Real) zu haben, lassen wir g : Real,Real -> Real | Nothing (Funktion) zu aus zwei reellen in (reelle oder nichts).

  4. Funktionen sollten aber (um einfacher zu sein) nur eins zurückgeben.

    Lösung: Erstellen wir einen neuen Datentyp, der zurückgegeben werden soll, einen " boxing type", der möglicherweise ein reales oder einfach nichts enthält. Daher können wir g : Real,Real -> Maybe Real Haben. OK aber ...

  5. Was passiert jetzt mit f(g(x,y))? f ist nicht bereit, einen Maybe Real Zu verbrauchen. Und wir möchten nicht jede Funktion ändern, die wir mit g verbinden könnten, um einen Maybe Real Zu verbrauchen.

    Lösung: Lassen Sie uns eine spezielle Funktion zum "Verbinden"/"Verfassen"/"Verknüpfen" von Funktionen haben. Auf diese Weise können wir hinter den Kulissen die Ausgabe einer Funktion anpassen, um die folgende zu speisen.

    In unserem Fall: g >>= f (Verbinden/komponieren Sie g mit f). Wir möchten, dass >>= Die Ausgabe von g erhält, sie überprüft und, falls es sich um Nothing handelt, einfach nicht f aufruft und Nothing; oder im Gegenteil, extrahieren Sie die Box Real und füttern Sie f damit. (Dieser Algorithmus ist nur die Implementierung von >>= Für den Typ Maybe).

  6. Es treten viele andere Probleme auf, die mit demselben Muster gelöst werden können: 1. Verwenden Sie eine "Box", um verschiedene Bedeutungen/Werte zu codieren/zu speichern, und haben Sie Funktionen wie g, die diese "Boxed Values" zurückgeben. 2. Lassen Sie Komponisten/Linker g >>= f Die Ausgabe von g mit der Eingabe von f verbinden, damit wir nicht f um ändern müssen alle.

  7. Bemerkenswerte Probleme, die mit dieser Technik gelöst werden können, sind:

    • einen globalen Zustand haben, den jede Funktion in der Folge von Funktionen ("das Programm") teilen kann: Lösung StateMonad.

    • Wir mögen keine "unreinen Funktionen": Funktionen, die unterschiedliche Ausgaben für gleiche Eingaben liefern. Markieren Sie daher diese Funktionen, damit sie einen getaggten Wert zurückgeben: IO monad.

Völliges Glück !!!!

289
cibercitizen1

In Begriffen, die ein OOP) Programmierer verstehen würde (ohne einen funktionalen Programmierhintergrund), was ist eine Monade?

Welches Problem löst es und an welchen Orten wird es am häufigsten verwendet? An welchen Orten wird es am häufigsten verwendet?

Im Sinne von OO) ist eine Monade eine durch einen Typ parametrisierte Schnittstelle (oder wahrscheinlicher ein Mixin) mit zwei Methoden, return und bind die beschreiben:

  • Wie man einen Wert injiziert, um einen monadischen Wert dieses injizierten Werttyps zu erhalten;
  • Verwendung einer Funktion, die einen monadischen Wert von einem nicht-monadischen Wert auf einen monadischen Wert umsetzt.

Das Problem, das es löst, ist die gleiche Art von Problem, die Sie von jeder Schnittstelle erwarten würden, nämlich "Ich habe eine Reihe verschiedener Klassen, die verschiedene Dinge tun, aber diese verschiedenen Dinge auf eine Weise zu tun scheinen, die eine zugrunde liegende Ähnlichkeit hat. Wie kann ich diese Ähnlichkeit zwischen ihnen beschreiben, auch wenn die Klassen selbst keine wirklich näheren Untertypen als die 'Objekt'-Klasse selbst sind? "

Genauer gesagt, die Monad "Schnittstelle" ähnelt IEnumerator oder IIterator darin, dass sie einen Typ annimmt, der selbst einen Typ annimmt. Der wichtigste "Punkt" von Monad ist jedoch die Möglichkeit, Operationen basierend auf dem Innentyp zu verbinden, sogar bis zu dem Punkt, dass ein neuer "interner Typ" vorliegt, während die Informationsstruktur von _ beibehalten oder sogar verbessert wird die Hauptklasse.

60
BMeph

Sie haben eine aktuelle Präsentation "Monadologie - professionelle Hilfe bei Typangst" von Christopher League (12. Juli 2010), der zu den Themen Fortsetzung und Monade sehr interessant ist.
Das Video zu dieser (Diashow-) Präsentation ist aktuell bei vimeo verfügbar.
Der Monadenteil beginnt in diesem einstündigen Video in ungefähr 37 Minuten und beginnt mit Folie 42 der Präsentation mit 58 Folien.

Es wird als "das führende Entwurfsmuster für funktionale Programmierung" vorgestellt, aber die in den Beispielen verwendete Sprache ist Scala, die sowohl OOP als auch funktional ist.
Mehr über Monad in Scala im Blog-Beitrag " Monaden - Ein anderer Weg, Berechnungen in Scala zu abstrahieren ", aus Debasish Ghosh (27. März 2008).

Ein Typ Konstruktor M ist eine Monade, wenn er diese Operationen unterstützt:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

Also zum Beispiel (in Scala):

  • Option ist eine Monade
 def unit [A] (x: A): Option [A] = Some (x) 
 
 def flatMap [A, B] (m: Option [A]) (f: A => Option [B]): Option [B] = 
 m passt zu {
 case None => None 
 case Some (x) => f (x ) 
} 
  • List ist Monad
 def unit [A] (x: A): Liste [A] = Liste (x) 
 
 def flatMap [A, B] (m: Liste [A]) (f: A => Liste [B]): Liste [B] = 
 m stimmt mit {
 case Nil => Nil 
 case x :: xs => f(x) ::: flatMap (xs) (f) 
} 

Monaden sind eine große Sache in Scala wegen der praktischen Syntax, die entwickelt wurde, um die Vorteile von Monadenstrukturen zu nutzen:

for Verständnis in Scala :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

wird vom Compiler übersetzt in:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

Die Schlüsselabstraktion ist das flatMap, das die Berechnung durch Verketten bindet.
Jeder Aufruf von flatMap gibt den gleichen Datenstrukturtyp (jedoch mit unterschiedlichem Wert) zurück, der als Eingabe für den nächsten Befehl in der Kette dient.

Im obigen Snippet nimmt flatMap als Eingabe einen Abschluss (SomeType) => List[AnotherType] Und gibt einen List[AnotherType] Zurück. Der wichtige Punkt ist, dass alle flatMaps den gleichen Abschlusstyp als Eingabe annehmen und den gleichen Typ als Ausgabe zurückgeben.

Dies ist es, was den Berechnungsthread "bindet" - jedes Element der Sequenz im For-Comprehension muss dieselbe Typeinschränkung einhalten.


Wenn Sie zwei Operationen ausführen (die möglicherweise fehlschlagen) und das Ergebnis an die dritte weitergeben, wie z.

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

aber ohne die Vorteile von Monad zu nutzen, erhalten Sie einen verschachtelten OOP-Code wie:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

in Monad hingegen können Sie mit den tatsächlichen Typen (Venue, User) arbeiten, so wie alle Vorgänge funktionieren, und das Zeug zur Optionsüberprüfung aufgrund der Flatmaps der for-Syntax verborgen lassen:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

Der Yield-Teil wird nur ausgeführt, wenn alle drei Funktionen Some[X] Haben; Jedes None wird direkt an confirm zurückgegeben.


Damit:

Monaden ermöglichen eine geordnete Berechnung in Functional Programing, mit der wir die Sequenzierung von Aktionen in einer gut strukturierten Form modellieren können, ähnlich einer DSL.

Die größte Stärke liegt in der Fähigkeit, Monaden für verschiedene Zwecke zu erweiterbaren Abstraktionen innerhalb einer Anwendung zusammenzustellen.

Diese Sequenzierung und das Threading von Aktionen durch eine Monade erfolgt durch den Sprachcompiler, der die Transformation durch die Magie der Abschlüsse vornimmt.


Übrigens ist Monad nicht nur ein Berechnungsmodell, das in FP verwendet wird:

Die Kategorietheorie schlägt viele Rechenmodelle vor. Unter ihnen

  • das Pfeilmodell der Berechnungen
  • das Monadenmodell der Berechnungen
  • das anwendbare Berechnungsmodell
42
VonC

Ich habe einen kurzen Artikel geschrieben, in dem Standard OOP python Code mit monadischem python Code verglichen wird, der den zugrunde liegenden Rechenprozess mit Diagrammen demonstriert Es werden keine Vorkenntnisse von FP vorausgesetzt. Ich hoffe, Sie finden es nützlich - http://nikgrozev.com/2013/12/10/monads-in-15-minutes/

34
Nikolay

Um schnelle Leser zu respektieren, beginne ich zuerst mit einer präzisen Definition, fahre dann mit einer schnelleren "Klartext" -Erklärung fort und gehe dann zu Beispielen über.

Hier ist eine kurze und genaue Definition leicht umformuliert:

Eine Monade (in der Informatik) ist formal eine Karte, die:

  • sendet jeden Typ X einer bestimmten Programmiersprache an einen neuen Typ T(X) (der als "Typ von T-Berechnungen mit Werten in X" bezeichnet wird);

  • ausgestattet mit einer Regel zum Zusammensetzen zweier Funktionen der Form f:X->T(Y) und g:Y->T(Z) zu einer Funktion g∘f:X->T(Z);

  • auf eine Weise, die im offensichtlichen Sinne assoziativ und in Bezug auf eine gegebene Einheitsfunktion, die als pure_X:X->T(X) bezeichnet wird, einheitlich ist, als würde sie einen Wert zur reinen Berechnung nehmen, die einfach diesen Wert zurückgibt.

In einfachen Worten ist eine Monade eine Regel, die von einem beliebigen Typ X an einen anderen Typ übergeben werden kann T(X) und eine Regel, die von zwei Funktionen f:X->T(Y) und g:Y->T(Z) (die Sie erstellen möchten, aber nicht können) an eine neue Funktion übergeben wird h:X->T(Z). Was jedoch nicht die Zusammensetzung im engeren mathematischen Sinne ist. Wir "biegen" im Grunde die Zusammensetzung einer Funktion oder definieren neu, wie Funktionen zusammengesetzt sind.

Außerdem benötigen wir die Kompositionsregel der Monade, um die "offensichtlichen" mathematischen Axiome zu erfüllen:

  • Assoziativität : Das Verfassen von f mit g und dann mit h (von außen) sollte dasselbe sein wie das Verfassen von g mit h und dann mit f (von innen).
  • Einheitliche Eigenschaft : Das Komponieren von f mit der Identitätsfunktion auf beiden Seiten sollte f ergeben.

Mit einfachen Worten, wir können nicht einfach verrückt werden, wenn wir unsere Funktionszusammensetzung neu definieren, wie wir es wollen:

  • Wir brauchen zuerst die Assoziativität, um mehrere Funktionen in einer Reihe zusammensetzen zu können, z. f(g(h(k(x))), und keine Sorge, die Reihenfolge anzugeben, in der die Funktionspaare zusammengesetzt werden. Da die Monadenregel nur vorschreibt, wie ein Paar von Funktionen zusammengesetzt wird, müssen wir ohne dieses Axiom wissen, welches Paar zuerst zusammengesetzt wird und so weiter. (Beachten Sie, dass dies nicht mit der Kommutativitätseigenschaft identisch ist, die f mit g erstellt hat, sondern mit g mit f, was nicht erforderlich ist.).
  • Und zweitens brauchen wir die einheitliche Eigenschaft, das heißt, Identitäten setzen sich trivial so zusammen, wie wir sie erwarten. So können wir Funktionen immer dann sicher umgestalten, wenn diese Identitäten extrahiert werden können.

Also noch einmal in Kürze: Eine Monade ist die Regel der Typerweiterung und der Kompositionsfunktionen, die die beiden Axiome - Assoziativität und Einheitseigenschaft - erfüllen.

In der Praxis möchten Sie, dass die Monade für Sie durch die Sprache, den Compiler oder das Framework implementiert wird, die bzw. das für Sie das Komponieren von Funktionen übernimmt. Sie können sich also darauf konzentrieren, die Logik Ihrer Funktion zu schreiben, anstatt sich Gedanken darüber zu machen, wie ihre Ausführung implementiert wird.

Das ist es im Wesentlichen, auf den Punkt gebracht.


Als professioneller Mathematiker ziehe ich es vor, h nicht als "Komposition" von f und g zu bezeichnen. Weil es mathematisch nicht so ist. Die Bezeichnung "Komposition" setzt fälschlicherweise voraus, dass h die wahre mathematische Komposition ist, die es nicht ist. Es wird nicht einmal eindeutig von f und g bestimmt. Stattdessen ist es das Ergebnis der neuen "Kompositionsregel" unserer Monade. Was sich von der tatsächlichen mathematischen Zusammensetzung völlig unterscheiden kann, auch wenn diese existiert!


Monade ist kein Funktor ! Ein Funktor F ist eine Regel vom Typ X zum Typ F(X) und Funktionen (Morphismus) zwischen den Typen X und Y zu Funktionen zwischen F(X) und F(Y) (Senden von Objekten an Objekte) und ihre Morphismen zu Morphismen in der Kategorietheorie). Stattdessen sendet eine Monade ein Paar von Funktionen f und g an eine neue h.


Um es weniger trocken zu machen, möchte ich versuchen, es anhand eines Beispiels zu veranschaulichen, das ich mit kleinen Abschnitten beschreibe, damit Sie direkt zum Punkt springen können.

Ausnahmewurf als Monadenbeispiel

Angenommen, wir möchten zwei Funktionen zusammensetzen:

f: x -> 1 / x
g: y -> 2 * y

Da jedoch f(0) nicht definiert ist, wird eine Ausnahme e ausgelöst. Wie können Sie dann den Kompositionswert g(f(0)) definieren? Wirf natürlich wieder eine Ausnahme! Vielleicht das gleiche e. Möglicherweise eine neue aktualisierte Ausnahme e1.

Was genau passiert hier? Erstens benötigen wir neue Ausnahmewerte (unterschiedlich oder gleich). Sie können sie nothing oder null oder was auch immer nennen, aber die Essenz bleibt gleich - es sollten neue Werte sein, z. In unserem Beispiel hier sollte es kein number sein. Ich ziehe es vor, sie nicht als null zu bezeichnen, um Verwechslungen mit der Implementierung von null in einer bestimmten Sprache zu vermeiden. Ebenso bevorzuge ich es, nothing zu meiden, weil es oft mit null assoziiert wird, was im Prinzip das ist, was null tun sollte, jedoch wird dieses Prinzip oft aus irgendwelchen praktischen Gründen verbogen.

Was ist eine Ausnahme genau?

Dies ist für jeden erfahrenen Programmierer eine triviale Angelegenheit, aber ich möchte nur ein paar Worte fallen lassen, um jegliche Verwirrung auszulöschen:

Ausnahme ist ein Objekt, das Informationen darüber enthält, wie das ungültige Ergebnis der Ausführung aufgetreten ist.

Dies kann vom Wegwerfen von Details bis zur Rückgabe eines einzelnen globalen Werts (wie NaN oder null) oder vom Generieren einer langen Protokollliste oder was genau passiert ist, Senden an eine Datenbank und Replizieren auf der gesamten verteilten Datenspeicherebene reichen.

Der wichtige Unterschied zwischen diesen beiden extremen Ausnahmebeispielen besteht darin, dass im ersten Fall keine Nebenwirkungen auftreten . In der zweiten gibt es. Was uns zur (Tausend-Dollar-) Frage bringt:

Sind Ausnahmen in reinen Funktionen erlaubt?

Kürzere Antwort : Ja, aber nur, wenn sie nicht zu Nebenwirkungen führen.

Längere Antwort Um rein zu sein, muss die Ausgabe Ihrer Funktion eindeutig durch ihre Eingabe bestimmt werden. Daher ändern wir unsere Funktion f, indem wir 0 An den neuen abstrakten Wert e senden, den wir als Ausnahme bezeichnen. Wir stellen sicher, dass der Wert e keine externen Informationen enthält, die nicht eindeutig durch unsere Eingabe bestimmt werden. Dies ist x. Hier ist ein Beispiel für eine Ausnahme ohne Nebenwirkung:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

Und hier ist eine mit Nebenwirkung:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

Tatsächlich hat es nur Nebenwirkungen, wenn sich diese Meldung möglicherweise in Zukunft ändern kann. Wenn sich dieser Wert jedoch garantiert nie ändert, ist er eindeutig vorhersehbar und es treten keine Nebenwirkungen auf.

Um es noch alberner zu machen. Eine Funktion, die 42 Zurückgibt, ist eindeutig rein. Aber wenn jemand, der verrückt ist, beschließt, 42 Zu einer Variablen zu machen, deren Wert sich ändern könnte, ist dieselbe Funktion unter den neuen Bedingungen nicht mehr rein.

Beachten Sie, dass ich der Einfachheit halber die Objektliteralnotation verwende, um das Wesentliche zu demonstrieren. Leider sind die Dinge in Sprachen wie JavaScript durcheinander, in denen error kein Typ ist, der sich in Bezug auf die Funktionszusammensetzung so verhält, wie wir es hier wollen, wohingegen tatsächliche Typen wie null oder NaN sich nicht so verhalten, sondern das eine künstliche und das andere durchlaufen Nicht immer intuitive Typkonvertierungen.

Typerweiterung

Da wir die Nachricht in unserer Ausnahme variieren wollen, deklarieren wir wirklich einen neuen Typ E für das gesamte Ausnahmeobjekt und dann ist es das, was maybe number Abgesehen von seinem verwirrenden Namen tut, der einer von beiden sein soll Geben Sie number oder den neuen Ausnahmetyp E ein, es ist also wirklich die Vereinigung number | E von number und E. Insbesondere hängt es davon ab, wie wir E konstruieren möchten, was im Namen maybe number Weder vorgeschlagen noch widergespiegelt wird.

Was ist funktionale Zusammensetzung?

Es ist die mathematische Operation, die Funktionen f: X -> Y Und g: Y -> Z Verwendet und deren Zusammensetzung als Funktion h: X -> Z Konstruiert, die h(x) = g(f(x)) erfüllt. Das Problem mit dieser Definition tritt auf, wenn das Ergebnis f(x) nicht als Argument von g zulässig ist.

In der Mathematik können diese Funktionen nicht ohne zusätzliche Arbeit komponiert werden. Die rein mathematische Lösung für unser obiges Beispiel von f und g besteht darin, 0 Aus der Definitionsmenge von f zu entfernen. Mit diesem neuen Definitionssatz (neuer restriktiverer Typ von x) wird f mit g zusammensetzbar.

Bei der Programmierung ist es jedoch nicht sehr praktisch, die Definitionsmenge von f auf diese Weise einzuschränken. Stattdessen können Ausnahmen verwendet werden.

Oder als ein anderer Ansatz werden künstliche Werte wie NaN, undefined, null, Infinity usw. erstellt. Sie bewerten also 1/0 Zu Infinity und 1/-0 Zu -Infinity. Und erzwingen Sie dann den neuen Wert zurück in Ihren Ausdruck, anstatt eine Ausnahme auszulösen. Dies kann zu vorhersehbaren oder nicht vorhersehbaren Ergebnissen führen:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

Und wir sind wieder bei den regulären Nummern und bereit weiterzumachen;)

JavaScript ermöglicht es uns, numerische Ausdrücke um jeden Preis auszuführen, ohne Fehler wie im obigen Beispiel zu verursachen. Das heißt, es können auch Funktionen zusammengestellt werden. Genau darum geht es in der Monade - es ist eine Regel, Funktionen zu komponieren, die die zu Beginn dieser Antwort definierten Axiome erfüllen.

Aber ist die Regel der Kompositionsfunktion, die sich aus der Implementierung von JavaScript für den Umgang mit numerischen Fehlern ergibt, eine Monade?

Um diese Frage zu beantworten, müssen Sie nur die Axiome überprüfen (als Übung hier nicht Teil der Frage belassen;).

Kann man mit einer Ausnahme eine Monade konstruieren?

In der Tat wäre eine nützlichere Monade die Regel, die vorschreibt, dass, wenn f für einige x eine Ausnahme auslöst, dies auch für die Zusammensetzung mit jedem g gilt. Machen Sie außerdem die Ausnahme E mit nur einem möglichen Wert ( Terminalobjekt in der Kategorietheorie) global eindeutig. Jetzt sind die beiden Axiome sofort überprüfbar und wir erhalten eine sehr nützliche Monade. Und das Ergebnis ist das, was als vielleicht Monade bekannt ist.

26
Dmitri Zaitsev

Eine Monade ist ein Datentyp, der einen Wert einkapselt und auf den im Wesentlichen zwei Operationen angewendet werden können:

  • return x Erstellt einen Wert des Monadentyps, der x einkapselt.
  • m >>= f (Als "Bindungsoperator" gelesen) wendet die Funktion f auf den Wert in der Monade m an.

Das ist was eine Monade ist. Es gibt ein paar weitere technische Details , aber im Grunde definieren diese beiden Operationen eine Monade. Die eigentliche Frage lautet: "Was macht eine Monade ?", Und das hängt von den Monaden ab - Listen sind Monaden, Maybes sind Monaden, IO Operationen sind Monaden. Wenn wir sagen, dass diese Dinge Monaden sind, bedeutet das nur, dass sie die Monadenschnittstelle von return und >>= Haben.

25
Chuck

Aus Wikipedia :

In der funktionalen Programmierung ist eine Monade eine Art abstrakter Datentyp, der zur Darstellung von Berechnungen verwendet wird (anstelle von Daten im Domänenmodell). Monaden ermöglichen es dem Programmierer, Aktionen zu verketten, um eine Pipeline zu erstellen, in der jede Aktion mit zusätzlichen Verarbeitungsregeln verziert ist, die von der Monade bereitgestellt werden. In funktionalem Stil geschriebene Programme können Monaden verwenden, um Prozeduren zu strukturieren, die sequenzierte Operationen enthalten, 1 [2] oder um beliebige Kontrollflüsse zu definieren (wie das Behandeln von Parallelität, Fortsetzungen oder Ausnahmen).

Formal wird eine Monade konstruiert, indem zwei Operationen (Binden und Zurückgeben) und ein Typkonstruktor M definiert werden, der mehrere Eigenschaften erfüllen muss, um die korrekte Zusammensetzung monadischer Funktionen zu ermöglichen (d. H. Funktionen, die Werte aus der Monade als Argumente verwenden). Die Rückgabeoperation nimmt einen Wert von einem einfachen Typ und legt ihn in einem monadischen Container des Typs M ab. Die Bindeoperation führt den umgekehrten Prozess aus, extrahiert den ursprünglichen Wert aus dem Container und übergibt ihn an die zugeordnete nächste Funktion in der Pipeline.

Ein Programmierer wird monadische Funktionen zusammenstellen, um eine Datenverarbeitungspipeline zu definieren. Die Monade fungiert als Framework, da es sich um ein wiederverwendbares Verhalten handelt, das über die Reihenfolge entscheidet, in der die spezifischen monadischen Funktionen in der Pipeline aufgerufen werden, und alle für die Berechnung erforderlichen Undercover-Arbeiten verwaltet. [3] Die Bind- und Return-Operatoren, die in der Pipeline verschachtelt sind, werden nach jeder Rückkehrsteuerung der monadischen Funktion ausgeführt und kümmern sich um die besonderen Aspekte, die von der Monade behandelt werden.

Ich glaube, das erklärt es sehr gut.

11
the_drow

Ich werde versuchen, die kürzeste Definition zu erstellen, die ich mit OOP Begriffen verwalten kann:

Eine generische Klasse CMonadic<T> Ist eine Monade, wenn sie mindestens die folgenden Methoden definiert:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}

und wenn die folgenden Gesetze für alle Typen T und ihre möglichen Werte t gelten

linke Identität:

CMonadic<T>.create(t).flatMap(f) == f(t)

richtige Identität

instance.flatMap(CMonadic<T>.create) == instance

assoziativität:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Beispiele:

Eine Listenmonade kann haben:

List<int>.create(1) --> [1]

Und flatMap auf der Liste [1,2,3] könnte so funktionieren:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iterables und Observables können ebenso monadisch gemacht werden wie Promises und Tasks.

Kommentar:

Monaden sind nicht so kompliziert. Die Funktion flatMap ähnelt stark der häufig anzutreffenden Funktion map. Es erhält ein Funktionsargument (auch als Delegat bezeichnet), das es (sofort oder später, null oder mehrmals) mit einem Wert aus der generischen Klasse aufrufen kann. Es wird erwartet, dass die übergebene Funktion ihren Rückgabewert auch in dieselbe Art von generischer Klasse einschließt. Um dies zu unterstützen, wird create bereitgestellt, ein Konstruktor, der aus einem Wert eine Instanz dieser generischen Klasse erstellen kann. Das Rückgabeergebnis von flatMap ist ebenfalls eine generische Klasse desselben Typs, die häufig dieselben Werte, die in den Rückgabeergebnissen einer oder mehrerer Anwendungen von flatMap enthalten waren, in die zuvor enthaltenen Werte packt. So können Sie flatMap beliebig verketten:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

Es kommt einfach so vor, dass diese Art von generischer Klasse als Basismodell für eine Vielzahl von Dingen nützlich ist. Dies (zusammen mit den Jargonismen der Kategorietheorie) ist der Grund, warum Monaden so schwer zu verstehen oder zu erklären scheinen. Sie sind sehr abstrakt und werden erst dann offensichtlich nützlich, wenn sie spezialisiert sind.

Beispielsweise können Sie Ausnahmen mit monadischen Containern modellieren. Jeder Container enthält entweder das Ergebnis der Operation oder den aufgetretenen Fehler. Die nächste Funktion (Delegat) in der Kette von flatMap-Rückrufen wird nur aufgerufen, wenn die vorherige einen Wert in den Container gepackt hat. Wenn andernfalls ein Fehler gepackt wurde, breitet sich der Fehler in den verketteten Containern weiter aus, bis ein Container gefunden wird, an den eine Fehlerbehandlungsfunktion mit der Methode .orElse() angehängt ist (eine solche Methode wäre eine zulässige Erweiterung).

Anmerkungen: Mit funktionalen Sprachen können Sie Funktionen schreiben, die für jede Art von monadischer generischer Klasse ausgeführt werden können. Damit dies funktioniert, müsste man eine generische Schnittstelle für Monaden schreiben. Ich weiß nicht, ob es möglich ist, eine solche Schnittstelle in C # zu schreiben, aber soweit ich weiß, ist es nicht:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad
}
11
Gorgi Kosev

Ob eine Monade eine "natürliche" Interpretation in OO hat, hängt von der Monade ab. In einer Sprache wie Java können Sie die Vielleicht-Monade in die Sprache der Suche nach Nullzeigern übersetzen, sodass Berechnungen, die fehlschlagen (d. H. In Haskell Nothing erzeugen), Nullzeiger als Ergebnisse ausgeben. Sie können die Statusmonade in die generierte Sprache übersetzen, indem Sie eine veränderbare Variable erstellen und Methoden zum Ändern ihres Status verwenden.

Eine Monade ist ein Monoid in der Kategorie der Endofunktoren.

Die Informationen, die dieser Satz zusammensetzt, sind sehr tief. Und Sie arbeiten in einer Monade mit jeder imperativen Sprache. Eine Monade ist eine "sequenzierte" domänenspezifische Sprache. Es erfüllt bestimmte interessante Eigenschaften, die zusammengenommen eine Monade zu einem mathematischen Modell der "imperativen Programmierung" machen. Mit Haskell ist es einfach, kleine (oder große) imperative Sprachen zu definieren, die auf verschiedene Arten kombiniert werden können.

Als OO Programmierer verwenden Sie die Klassenhierarchie Ihrer Sprache, um die Arten von Funktionen oder Prozeduren zu organisieren, die in einem Kontext aufgerufen werden können, den Sie ein Objekt nennen. Eine Monade ist auch eine Abstraktion dieser Idee, sofern verschiedene Monaden auf beliebige Weise kombiniert werden können, wodurch alle Methoden der Untermonade effektiv in den Geltungsbereich "importiert" werden.

In der Architektur verwendet man dann Typensignaturen, um explizit auszudrücken, welche Kontexte zur Berechnung eines Werts verwendet werden können.

Zu diesem Zweck kann man Monadentransformatoren verwenden, und es gibt eine hochwertige Sammlung aller "Standard" -Monaden:

  • Listen (nicht deterministische Berechnungen, indem eine Liste als Domäne behandelt wird)
  • Vielleicht (Berechnungen, die fehlschlagen können, für die die Berichterstellung jedoch unwichtig ist)
  • Fehler (Berechnungen, die fehlschlagen können und eine Ausnahmebehandlung erfordern
  • Reader (Berechnungen, die durch Kompositionen einfacher Haskell-Funktionen dargestellt werden können)
  • Writer (Berechnungen mit sequentiellem "Rendern"/"Protokollieren" (in Strings, HTML usw.)
  • Fortsetzung
  • IO (Berechnungen, die vom zugrunde liegenden Computersystem abhängen)
  • State (Berechnungen, deren Kontext einen veränderbaren Wert enthält)

mit entsprechenden monadentransformatoren und typklassen. Typklassen ermöglichen einen komplementären Ansatz zum Kombinieren von Monaden durch Vereinheitlichen ihrer Schnittstellen, so dass konkrete Monaden eine Standardschnittstelle für die Monaden "Art" implementieren können. Beispielsweise enthält das Modul Control.Monad.State eine Klasse MonadState s m, und (State s) ist eine Instanz des Formulars

instance MonadState s (State s) where
    put = ...
    get = ...

Die lange Geschichte ist, dass eine Monade ein Funktor ist, der "Kontext" mit einem Wert verknüpft, der die Möglichkeit bietet, einen Wert in die Monade einzufügen, und der zumindest die Möglichkeit bietet, Werte in Bezug auf den damit verbundenen Kontext zu bewerten in eingeschränkter Weise.

Damit:

return :: a -> m a

ist eine Funktion, die einen Wert vom Typ a in eine Monaden- "Aktion" vom Typ ma einfügt.

(>>=) :: m a -> (a -> m b) -> m b

ist eine Funktion, die eine Monadenaktion ausführt, ihr Ergebnis bewertet und eine Funktion auf das Ergebnis anwendet. Das Schöne an (>> =) ist, dass das Ergebnis in derselben Monade vorliegt. Mit anderen Worten, in m >> = f zieht (>> =) das Ergebnis aus m heraus und bindet es an f, so dass das Ergebnis in der Monade ist. (Alternativ können wir sagen, dass (>> =) f in m zieht und auf das Ergebnis anwendet.) Wenn wir also f :: a -> mb und g :: b -> mc haben, können wir das "Sequenz" Aktionen:

m >>= f >>= g

Oder mit "do notation"

do x <- m
   y <- f x
   g y

Der Typ für (>>) leuchtet möglicherweise. Es ist

(>>) :: m a -> m b -> m b

Es entspricht dem (;) Operator in prozeduralen Sprachen wie C. Es erlaubt Notationen wie:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

In der mathematischen und philosophischen Logik haben wir Rahmen und Modelle, die "natürlich" mit Monadismus modelliert sind. Eine Interpretation ist eine Funktion, die in den Bereich des Modells schaut und den Wahrheitswert (oder die Verallgemeinerungen) eines Satzes (oder einer Formel unter Verallgemeinerungen) berechnet. In einer modalen Logik für die Notwendigkeit könnten wir sagen, dass ein Satz notwendig ist, wenn er in "jeder möglichen Welt" wahr ist - wenn er in Bezug auf jeden zulässigen Bereich wahr ist. Dies bedeutet, dass ein Modell in einer Sprache für einen Satz als Modell reifiziert werden kann, dessen Domäne aus einer Sammlung unterschiedlicher Modelle besteht (eines, das jeder möglichen Welt entspricht). Jede Monade hat eine Methode mit dem Namen "join", die Ebenen abflacht, was bedeutet, dass jede Monadenaktion, deren Ergebnis eine Monadenaktion ist, in die Monade eingebettet werden kann.

join :: m (m a) -> m a

Noch wichtiger ist, dass die Monade unter der Operation "Layer Stacking" geschlossen wird. So funktionieren Monadentransformatoren: Sie kombinieren Monaden, indem sie "join-like" -Methoden für Typen wie bereitstellen

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

damit können wir eine Aktion in (VielleichtT m) in eine Aktion in m umwandeln und Ebenen effektiv kollabieren. In diesem Fall ist runMaybeT :: MaybeT m a -> m (Maybe a) unsere join-ähnliche Methode. (MaybeT m) ist eine Monade, und MaybeT :: m (Maybe a) -> MaybeT m a ist effektiv ein Konstruktor für eine neue Art von Monadenaktion in m.

Eine freie Monade für einen Funktor ist die durch Stapeln von f erzeugte Monade, mit der Folgerung, dass jede Folge von Konstruktoren für f ein Element der freien Monade ist (oder genauer gesagt, etwas mit der gleichen Form wie der Baum von Folgen von Konstruktoren für f). Freie Monaden sind eine nützliche Technik zum Aufbau flexibler Monaden mit minimaler Menge an Kesselplatte. In einem Haskell-Programm könnte ich freie Monaden verwenden, um einfache Monaden für die "High-Level-Systemprogrammierung" zu definieren, um die Typensicherheit zu gewährleisten (ich verwende nur Typen und ihre Deklarationen. Die Implementierung ist mit der Verwendung von Kombinatoren einfach):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

Monadismus ist die zugrunde liegende Architektur für das, was man als "Interpreter" - oder "Befehls" -Muster bezeichnen könnte, abstrahiert in seiner klarsten Form, da jede monadische Berechnung zumindest trivial "ausgeführt" werden muss. (Das Laufzeitsystem führt die IO Monade für uns aus und ist der Einstiegspunkt für jedes Haskell-Programm. IO "treibt" den Rest der Berechnungen durch Ausführen von IO Aktionen in Reihenfolge).

Der Typ für Join ist auch der Punkt, an dem wir die Aussage erhalten, dass eine Monade ein Monoid in der Kategorie der Endofunktoren ist. Join ist aufgrund seines Typs für theoretische Zwecke in der Regel wichtiger. Aber den Typ zu verstehen bedeutet, Monaden zu verstehen. Join- und Monadentransformatortypen sind effektiv Zusammensetzungen von Endofunktoren im Sinne der Funktionszusammensetzung. Um es in eine Haskell-ähnliche Pseudo-Sprache zu bringen,

Foo :: m (ma) <-> (m. M) a

7
nomen

Eine Monade ist ein Array von Funktionen

(Pst: ein Array von Funktionen ist nur eine Berechnung).

Anstelle eines echten Arrays (eine Funktion in einem Zellenarray) haben Sie diese Funktionen durch eine andere Funktion >> = verkettet. Mit >> = können Sie die Ergebnisse der Funktion i an die Vorschubfunktion i + 1 anpassen, Berechnungen zwischen ihnen durchführen oder sogar die Funktion i + 1 nicht aufrufen.

Die hier verwendeten Typen sind "Typen mit Kontext". Dies ist ein Wert mit einem "Tag". Die Funktionen, die verkettet werden, müssen einen "nackten Wert" annehmen und ein mit Tags versehenes Ergebnis zurückgeben. Eine der Aufgaben von >> = ist es, einen nackten Wert aus seinem Kontext zu extrahieren. Es gibt auch die Funktion "return", die einen nackten Wert annimmt und mit einem Tag versieht.

Ein Beispiel mit Vielleicht . Verwenden wir es, um eine einfache Ganzzahl zu speichern, für die Berechnungen durchgeführt werden.

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

Um zu zeigen, dass Monaden ein Array von Funktionen mit Hilfsoperationen sind, betrachten Sie das Äquivalent zum obigen Beispiel, indem Sie nur ein reales Array von Funktionen verwenden

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

Und es würde so verwendet werden:

print (runMyMonad (Just 160) myArray1)
3
cibercitizen1

Eine einfache Erklärung für Monaden mit einer Fallstudie von Marvel lautet hier .

Monaden sind Abstraktionen, die verwendet werden, um abhängige Funktionen zu sequenzieren, die wirksam sind. Hier bedeutet dies, dass sie einen Typ in Form von F [A] zurückgeben, beispielsweise Option [A], wobei Option F ist und als Typkonstruktor bezeichnet wird. Lassen Sie uns dies in 2 einfachen Schritten sehen

  1. Unten Die Funktionszusammensetzung ist transitiv. Um also von A nach C zu gelangen, kann ich A => B und B => C zusammensetzen.
 A => C   =   A => B  andThen  B => C

enter image description here

  1. Wenn die Funktion jedoch einen Effekttyp wie Option [A] zurückgibt, d. H. A => F [B], funktioniert die Komposition nicht, um zu B zu gelangen. Wir brauchen A => B, aber wir haben A => F [B].
    enter image description here

    Wir brauchen einen speziellen Operator, "bind", der weiß, wie man diese Funktionen, die F [A] zurückgeben, verschmilzt.

 A => F[C]   =   A => F[B]  bind  B => F[C]

Die Funktion "binden" ist für die spezifische Funktion [~ # ~] f [~ # ~] definiert.

Es gibt auch "return" vom Typ A => F [A] für jedes [~ # ~] a [~ # ~ ], definiert für das spezifische [~ # ~] f [~ # ~] auch. Um eine Monade zu sein, müssen [~ # ~] f [~ # ~] diese beiden Funktionen definiert sein.

So können wir aus jeder reinen Funktion eine wirksame Funktion A => F [B] konstruieren A => B,

 A => F[B]   =   A => B  andThen  return

aber ein gegebenes [~ # ~] f [~ # ~] kann auch seine eigenen undurchsichtigen "eingebauten" Sonderfunktionen von solchen Typen definieren, die ein Benutzer nicht selbst definieren kann (in a pur Sprache), wie

  • "random" ( Range => Random [Int])
  • "print" ( String => IO [()])
  • "try ... catch" usw.
2
Ira

Monaden im typischen Gebrauch sind das funktionale Äquivalent zu den Ausnahmebehandlungsmechanismen der prozeduralen Programmierung.

In modernen prozeduralen Sprachen setzen Sie einen Ausnahmehandler um eine Folge von Anweisungen, von denen jede eine Ausnahme auslösen kann. Wenn eine der Anweisungen eine Ausnahme auslöst, wird die normale Ausführung der Anweisungsfolge angehalten und an einen Ausnahmebehandler übertragen.

Funktionale Programmiersprachen vermeiden jedoch philosophisch Ausnahmebehandlungsmerkmale aufgrund der "goto" -ähnlichen Natur derselben. Die funktionale Programmierperspektive ist, dass Funktionen keine "Nebenwirkungen" haben sollten, wie Ausnahmen, die den Programmfluss stören.

In der Realität sind Nebenwirkungen vor allem durch I/O nicht auszuschließen. Monaden in der funktionalen Programmierung werden verwendet, um dies zu handhaben, indem eine Reihe von verketteten Funktionsaufrufen (von denen jeder ein unerwartetes Ergebnis erzeugen kann) und jedes unerwartete Ergebnis in gekapselte Daten umgewandelt werden, die die verbleibenden Funktionsaufrufe noch sicher durchlaufen können.

Der Kontrollfluss bleibt erhalten, aber das unerwartete Ereignis wird sicher gekapselt und behandelt.

2
David K. Hess

In OO) Ausdrücken ist eine Monade ein flüssiger Behälter.

Die Mindestanforderung ist eine Definition von class <A> Something, Die einen Konstruktor Something(A a) und mindestens eine Methode Something<B> flatMap(Function<A, Something<B>>) unterstützt.

Wahrscheinlich zählt es auch, wenn Ihre Monadenklasse Methoden mit der Signatur Something<B> work() hat, die die Regeln der Klasse beibehalten - der Compiler wird zur Kompilierungszeit in flatMap gebacken.

Warum ist eine Monade nützlich? Weil es ein Container ist, der verkettbare Operationen ermöglicht, die die Semantik bewahren. Beispielsweise behält Optional<?> Die Semantik von isPresent für Optional<String>, Optional<Integer>, Optional<MyClass> Usw. bei.

Als grobes Beispiel

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Beachten Sie, dass wir mit einer Zeichenfolge beginnen und mit einer Ganzzahl enden. Ziemlich cool.

In OO kann es etwas dauern, bis die Handbewegung einsetzt, aber jede Methode für Something, die eine andere Unterklasse von Something zurückgibt, erfüllt das Kriterium einer Containerfunktion, die einen Container des ursprünglichen Typs zurückgibt.

So behalten Sie die Semantik bei - d. H. Die Bedeutung und die Operationen des Containers ändern sich nicht, sie umschließen und optimieren lediglich das Objekt im Container.

1
Rob

Siehe mein Antwort zu "Was ist eine Monade?"

Es beginnt mit einem motivierenden Beispiel, arbeitet sich durch das Beispiel, leitet ein Beispiel für eine Monade ab und definiert formal "Monade".

Es setzt keine Kenntnisse der funktionalen Programmierung voraus und verwendet Pseudocode mit function(argument) := expression -Syntax mit möglichst einfachen Ausdrücken.

Dieses C++ - Programm ist eine Implementierung der Pseudocode-Monade. (Als Referenz: M ist der Typkonstruktor, feed ist die "Bind" -Operation und wrap ist die "Return" -Operation.)

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}
1
Jordan

Wenn Sie jemals Powershell verwendet haben, sollten die von Eric beschriebenen Muster vertraut klingen. Powershell-Cmdlets sind Monaden; Die funktionale Zusammensetzung wird durch eine Pipeline dargestellt.

Jeffrey Snovers Interview mit Erik Meijer geht näher darauf ein.

1
Richard Berg

Ich teile mein Verständnis von Monaden, die theoretisch vielleicht nicht perfekt sind. Monaden handeln von Kontexterweiterung. Monad ist, Sie definieren einen Kontext für einige Daten und legen dann fest, wie der Kontext mit den Daten in der gesamten Verarbeitungspipeline übertragen wird. Bei der Weitergabe definierender Kontexte geht es hauptsächlich darum, festzulegen, wie mehrere Kontexte zusammengeführt werden sollen. Es wird auch sichergestellt, dass Kontexte nicht versehentlich aus den Daten entfernt werden. Dieses einfache Konzept kann verwendet werden, um die Kompilierzeitkorrektheit eines Programms sicherzustellen.

0
Gulshan

Aus praktischer Sicht (was in vielen vorhergehenden Antworten und verwandten Artikeln zusammengefasst wurde) scheint es mir, dass einer der grundlegenden "Zwecke" (oder der Nutzen) der Monade darin besteht, die Abhängigkeiten zu nutzen, die in rekursiven Methodenaufrufen impliziert sind Auch bekannt als Funktionskomposition (dh wenn f1 f2 f3 aufruft, muss f3 vor f2 vor f1 ausgewertet werden), um sequentielle Komposition auf natürliche Weise darzustellen, insbesondere im Kontext eines Lazy-Evaluation-Modells (dh sequentielle Komposition als einfache Sequenz) zB "f3 (); f2 (); f1 ();" in C - der Trick ist besonders offensichtlich, wenn Sie an einen Fall denken, in dem f3, f2 und f1 tatsächlich nichts zurückgeben [ihre Verkettung als f1 (f2 (f3)) ist künstlich, lediglich dazu gedacht, eine Sequenz zu erzeugen]).

Dies ist besonders relevant, wenn es sich um Nebenwirkungen handelt, dh wenn sich ein Zustand ändert (wenn f1, f2, f3 keine Nebenwirkungen hatten, wäre es egal, in welcher Reihenfolge sie ausgewertet werden; das ist eine großartige Eigenschaft von pure funktionale Sprachen, um diese Berechnungen beispielsweise parallelisieren zu können). Je mehr reine Funktionen, desto besser.

Ich denke aus diesem engen Blickwinkel, Monaden könnten als syntaktischer Zucker für Sprachen angesehen werden, die eine faule Bewertung bevorzugen (die Dinge nur dann bewerten, wenn dies absolut notwendig ist, in einer Reihenfolge, die sich nicht auf die Darstellung des Codes stützt) und die keine haben andere Mittel zur Darstellung der sequentiellen Komposition. Das Nettoergebnis ist, dass Codeabschnitte, die "unrein" sind (dh Nebenwirkungen haben), zwingend auf natürliche Weise dargestellt werden können, jedoch sauber von reinen Funktionen (ohne Nebenwirkungen) getrennt werden können faul bewertet.

Dies ist jedoch nur ein Aspekt, wie gewarnt hier .

0
novis