877 Stimmen

Monade in einfachem Englisch (für den OOP-Programmierer ohne FP-Hintergrund)

Was ist eine Monade in Begriffen, die ein OOP-Programmierer verstehen würde (ohne Hintergrundwissen über funktionale Programmierung)?

Welches Problem wird damit gelöst und wo wird es am häufigsten eingesetzt?

Update

Um das Verständnis zu verdeutlichen, nach dem ich gesucht habe, nehmen wir an, Sie würden eine FP-Anwendung mit Monaden in eine OOP-Anwendung konvertieren. Was würden Sie tun, um die Aufgaben der Monaden in die OOP-Anwendung zu übertragen?

10 Stimmen

0 Stimmen

Da praktisch jeder, der sich mit Monaden befasst, bereits ein paar OOP-Sprachen kennt, wird die Frage "Erklären Sie Monaden" in der Regel für OOP-Programmierer beantwortet. Außerdem bezieht sich der von Pascal empfohlene Blogbeitrag auf C++ als Beispielsprache. Daher habe ich dafür gestimmt, den Beitrag als Duplikat zu schließen (von stackoverflow.com/questions/2366/can-anyone-explain-monads ), und ich rate allen, dasselbe zu tun.

848voto

Eric Lippert Punkte 628543

UPDATE: Diese Frage war Gegenstand einer langen Blogserie, die Sie unter folgender Adresse lesen können Monaden - Danke für die tolle Frage!

Was ist eine Monade in Begriffen, die ein OOP-Programmierer verstehen würde (ohne Hintergrundwissen über funktionale Programmierung)?

Eine Monade ist eine "Verstärker" von Typen que sich an bestimmte Regeln hält y die bestimmte Maßnahmen vorsieht .

Erstens, was ist ein "Verstärker von Typen"? Damit meine ich ein System, mit dem man einen Typ in einen spezielleren Typ umwandeln kann. Betrachten Sie zum Beispiel in C# Nullable<T> . Dies ist ein Verstärker von Typen. Sie können einen Typ nehmen, sagen wir int und fügen diesem Typ eine neue Fähigkeit hinzu, nämlich, dass er jetzt null sein kann, was vorher nicht möglich war.

Ein zweites Beispiel ist IEnumerable<T> . Es ist ein Verstärker von Typen. Es lässt Sie einen Typ nehmen, sagen wir, string und fügen diesem Typ eine neue Fähigkeit hinzu, nämlich die, dass man nun aus einer beliebigen Anzahl von Einzelstrings eine Folge von Strings machen kann.

Was sind die "sicheren Regeln"? Kurz gesagt, dass es einen sinnvollen Weg gibt, wie Funktionen auf dem zugrunde liegenden Typ auf dem verstärkten Typ funktionieren, so dass sie den normalen Regeln der funktionalen Komposition folgen. Wenn Sie zum Beispiel eine Funktion auf Ganzzahlen haben, sagen wir

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

dann ist die entsprechende Funktion auf Nullable<int> kann dafür sorgen, dass alle Operatoren und Anrufe dort "auf die gleiche Weise" zusammenarbeiten wie zuvor.

(Das ist unglaublich vage und ungenau; Sie haben um eine Erklärung gebeten, die keine Kenntnisse über die funktionale Zusammensetzung voraussetzt).

Was sind die "Operationen"?

  1. Es gibt eine "unit"-Operation (verwirrenderweise manchmal "return"-Operation genannt), die einen Wert eines einfachen Typs annimmt und den entsprechenden monadischen Wert erzeugt. Dies bietet im Wesentlichen eine Möglichkeit, einen Wert eines unverstärkten Typs in einen Wert des verstärkten Typs zu verwandeln. Dies könnte als Konstruktor in einer OO-Sprache implementiert werden.

  2. Es gibt eine "Bind"-Operation, die einen monadischen Wert und eine Funktion annimmt, die den Wert umwandeln kann, und einen neuen monadischen Wert zurückgibt. Bind ist die Schlüsseloperation, die die Semantik der Monade definiert. Mit ihr können wir Operationen auf dem unverstärkten Typ in Operationen auf dem verstärkten Typ umwandeln, die den zuvor erwähnten Regeln der funktionalen Komposition gehorchen.

  3. Oft gibt es eine Möglichkeit, den unverstärkten Typ wieder aus dem verstärkten Typ herauszuholen. Streng genommen ist diese Operation nicht erforderlich, um eine Monade zu haben. (Obwohl sie notwendig ist, wenn man eine comonad . Wir werden diese in diesem Artikel nicht weiter betrachten).

Nochmals, nimm Nullable<T> als Beispiel. Sie können eine int in eine Nullable<int> mit dem Konstruktor. Der C#-Compiler kümmert sich um die meisten nullable "Aufhebung" für Sie, aber wenn es nicht, die Aufhebung Transformation ist einfach: eine Operation, sagen,

int M(int x) { whatever }

wird umgewandelt in

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

Und das Drehen eines Nullable<int> zurück in eine int erfolgt mit dem Value Eigentum.

Die Funktionsumwandlung ist der springende Punkt. Beachten Sie, dass die eigentliche Semantik der Nullable-Operation - dass eine Operation auf einer null propagiert die null - wird in der Transformation erfasst. Wir können dies verallgemeinern.

Angenommen, Sie haben eine Funktion aus int à int wie unser Original M . Sie können dies leicht in eine Funktion umwandeln, die eine int und gibt eine Nullable<int> weil Sie das Ergebnis einfach durch den nullbaren Konstruktor laufen lassen können. Nehmen wir nun an, 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 man damit machen kann? Jede Methode, die eine int und gibt eine int , oder nimmt eine int und gibt eine Nullable<int> kann nun die "nullable"-Semantik auf sie angewendet werden .

Außerdem: Nehmen wir an, Sie haben zwei Methoden

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

und Sie wollen sie komponieren:

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

Das heißt, Z ist die Zusammensetzung aus X y Y . Aber das können Sie nicht tun, weil X nimmt eine int y Y gibt eine Nullable<int> . Aber da Sie die "bind"-Operation haben, können Sie das schaffen:

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

Die Bind-Operation auf einer Monade ist das, was die Komposition von Funktionen auf verstärkten Typen ermöglicht. Die "Regeln", die ich oben erwähnt habe, sind, dass die Monade die Regeln der normalen Funktionskomposition beibehält; dass die Komposition mit Identitätsfunktionen die ursprüngliche Funktion ergibt, dass die Komposition assoziativ ist und so weiter.

In C# wird "Bind" als "SelectMany" bezeichnet. Schauen Sie sich an, wie es in der Sequenzmonade funktioniert. Wir brauchen zwei Dinge: einen Wert in eine Sequenz umwandeln und Operationen auf Sequenzen binden. Als Bonus haben wir auch "eine Sequenz zurück 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 Monadenregel lautete: "Um zwei Funktionen zu kombinieren, die nullables erzeugen, prüfe, ob die innere Funktion null ergibt; wenn ja, erzeuge null, wenn nicht, rufe die äußere Funktion mit dem Ergebnis auf". Das ist die gewünschte Semantik von nullable.

Die Regel der Sequenzmonade lautet: "Zwei Funktionen, die Sequenzen erzeugen, miteinander kombinieren, die äußere Funktion auf jedes Element anwenden, das von der inneren Funktion erzeugt wird, und dann alle resultierenden Sequenzen miteinander verketten". Die grundlegende Semantik der Monaden ist in der Bind / SelectMany Methoden; dies ist die Methode, die Ihnen sagt, was die Monade wirklich bedeutet .

Wir können es sogar noch besser machen. Nehmen wir an, Sie haben eine Folge von Ints und eine Methode, die Ints annimmt und eine Folge von Strings liefert. 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 der einen 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: "Erweitern Sie diesen Haufen einzelner Ganzzahlen zu einer Folge von Ganzzahlen. Transformieren Sie diese bestimmte ganze Zahl in eine Reihe von Zeichenketten, die zu einer Folge von Zeichenketten erweitert wurden. Jetzt fügen wir beide Operationen zusammen: Erweitern Sie dieses Bündel von Ganzzahlen in die Verkettung aller Zeichenkettenfolgen." Mit Monaden können Sie verfassen. Ihre Erweiterungen.

Welches Problem wird damit gelöst und wo wird es am häufigsten eingesetzt?

Das ist so ähnlich wie die Frage "Welche Probleme löst das Singleton-Muster?", aber ich werde es versuchen.

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

  • Ich muss neue Funktionen für diesen Typ erstellen und trotzdem alte Funktionen mit diesem Typ kombinieren, um die neuen Funktionen zu nutzen.
  • Ich muss eine Reihe von Operationen auf Typen erfassen und diese Operationen als zusammensetzbare Objekte darstellen, indem ich immer größere Kompositionen aufbaue, bis ich genau die richtige Reihe von Operationen dargestellt habe, und dann muss ich anfangen, Ergebnisse aus dem Ding herauszuholen
  • Ich benötige eine saubere Darstellung von Operationen mit Seiteneffekten in einer Sprache, die Seiteneffekte hasst

C# verwendet Monaden in seinem Design. Wie bereits erwähnt, ist das Nullable-Muster der "Maybe-Monade" sehr ähnlich. LINQ ist vollständig aus Monaden aufgebaut; die SelectMany Methode übernimmt die semantische Arbeit der Komposition von Operationen. (Erik Meijer weist gerne darauf hin, dass jede LINQ-Funktion eigentlich durch SelectMany alles andere ist nur eine Bequemlichkeit).

Um das Verständnis zu verdeutlichen, nach dem ich gesucht habe, nehmen wir an, Sie würden eine FP-Anwendung, die Monaden enthält, in eine OOP-Anwendung konvertieren. Was würden Sie tun, um die Aufgaben der Monaden in die OOP-Anwendung zu übertragen?

Die meisten OOP-Sprachen verfügen nicht über ein ausreichend reichhaltiges Typsystem, um das Monadenmuster selbst direkt abzubilden; Sie benötigen ein Typsystem, das Typen unterstützt, die höhere Typen als generische Typen sind. Ich würde also nicht versuchen, das zu tun. Stattdessen würde ich generische Typen implementieren, die jede Monade repräsentieren, und Methoden implementieren, die die drei von Ihnen benötigten Operationen darstellen: Umwandlung eines Wertes in einen verstärkten Wert, (vielleicht) Umwandlung eines verstärkten Wertes in einen Wert und Umwandlung einer Funktion auf unverstärkten Werten in eine Funktion auf verstärkten Werten.

Ein guter Anfang ist die Implementierung von LINQ in C#. Studieren Sie die SelectMany Methode; sie ist der Schlüssel zum Verständnis, wie die Sequenzmonade in C# funktioniert. Es ist eine sehr einfache Methode, aber sehr mächtig!


Empfohlene, weiterführende Lektüre:

  1. Für eine tiefer gehende und theoretisch fundierte Erklärung von Monaden in C# empfehle ich meine ( Eric Lippert Der Artikel des Kollegen Wes Dyer zu diesem Thema. Dieser Artikel hat mir die Monaden erklärt, als es bei mir endlich "klick" machte.
  2. Eine gute Veranschaulichung, warum man eine Monade um (verwendet Haskell in seinen Beispielen) .
  3. Eine Art "Übersetzung" des vorherigen Artikels in JavaScript.

20 Stimmen

Das ist eine großartige Antwort, aber mir ist der Kopf schwindelig geworden. Ich werde sie am Wochenende weiterverfolgen und Ihnen Fragen stellen, wenn sich die Dinge in meinem Kopf nicht beruhigen und einen Sinn ergeben.

6 Stimmen

Ausgezeichnete Erklärung wie immer, Eric. Für eine eher theoretische (aber immer noch hochinteressante) Diskussion fand ich Bart De Smets Blogbeitrag über MinLINQ hilfreich, der einige funktionale Programmierkonstrukte auch auf C# zurückführt. community.bartdesmet.net/blogs/bart/archive/2010/01/01/

49 Stimmen

Für mich macht es mehr Sinn, zu sagen Erweitert Typen und nicht verstärkt sie.

438voto

cibercitizen1 Punkte 19984

Warum brauchen wir Monaden?

  1. Wir wollen programmieren nur mit Funktionen . ("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 zuerst ausgeführt werden soll ? Wie kann man eine geordnete Folge von Funktionen bilden (d.h. ein Programm ) mit nicht mehr als Funktionen?

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

  3. Weitere Probleme: einige Funktionen könnte scheitern (d.h. g(2,0) , durch 0 teilen). Wir haben keine "Ausnahmen" im FP . Wie können wir das Problem lösen?

    Lösung: Lassen Sie uns erlauben es Funktionen, zwei Arten von Dingen zurückzugeben : statt mit g : Real,Real -> Real (Funktion von zwei reellen Werten in einen reellen Wert), erlauben wir g : Real,Real -> Real | Nothing (Funktion von zwei Realen in (real oder nichts)).

  4. Aber Funktionen sollten (der Einfachheit halber) nur eine Sache .

    Lösung: Erstellen wir einen neuen Datentyp, der zurückgegeben werden soll, ein " Boxenart ", das vielleicht ein Reales einschließt oder einfach nichts ist. Folglich können wir haben g : Real,Real -> Maybe Real . OK, aber ...

  5. Was geschieht nun mit f(g(x,y)) ? f nicht bereit ist, eine Maybe Real . Und wir wollen nicht jede Funktion ändern, die wir mit g zum Verzehr einer Maybe Real .

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

    In unserem Fall: g >>= f (verbinden/zusammensetzen g à f ). Wir wollen >>= zu erhalten g überprüfen Sie die Ausgabe und, falls es sich um Nothing Rufen Sie einfach nicht an f und Rückkehr Nothing ; oder im Gegenteil, die verpackten Real und Futtermittel f mit ihm. (Dieser Algorithmus ist lediglich die Implementierung von >>= für die Maybe Typ).

  6. Es gibt viele andere Probleme, die nach dem gleichen Muster gelöst werden können: 1. eine "Box" verwenden, um verschiedene Bedeutungen/Werte zu kodieren/zu speichern und Funktionen wie g die diese "verpackten Werte" zurückgeben. 2. Komponisten/Verlinker haben g >>= f zur Unterstützung der Verbindung g Ausgabe an f eingeben, so dass wir nicht ändern müssen f überhaupt nicht.

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

    • mit einem globalen Zustand, den jede Funktion in der Abfolge von Funktionen ("das Programm") gemeinsam nutzen kann: Lösung StateMonad .

    • Wir mögen keine "unreinen Funktionen": Funktionen, die zu verschiedene Ausgabe für dieselbe Eingabe. Deshalb sollten wir diese Funktionen markieren, damit sie einen getaggten/boxierten Wert zurückgeben: IO Monade.

Totales Glück !!!!

1 Stimmen

"allow g : Real,Real -> Real | Nothing" ist keine gute Lösung, da sie zu weiteren Problemen führt, z.B. was ist Real + Nothing? Ausnahmen zulassen ist sauberer und vorhersehbarer und außerdem mathematisch korrekt.

2 Stimmen

@DmitriZaitsev Ausnahmen können nur in "unreinem Code" (der IO-Monade) auftreten, soweit ich weiß.

1 Stimmen

Warum nicht? Eine Ausnahme ist eine Ausgabe, die eindeutig durch die Eingabe bestimmt wird. Das wäre dasselbe wie eine Monade, die immer nichts von nichts zurückgibt, wenn ich es richtig verstehe. Während jede andere "Link"-Funktion zu schwer zu findenden Fehlern führen könnte, was z.B. bei NaN die jeder hasst.

103voto

JacquesB Punkte 40790

Ich würde sagen, die nächste OO-Analogie zu Monaden ist die " Befehlsmuster ".

Im Befehlsmuster verpacken Sie eine gewöhnliche Anweisung oder einen Ausdruck in eine Befehl Objekt. Das Befehlsobjekt stellt ein ausführen. Methode, die die umgeschlagene Anweisung ausführt. So werden die Anweisungen zu Objekten erster Klasse, die beliebig weitergegeben und ausgeführt werden können. Befehle können sein verfasst Sie können also ein Programm-Objekt durch Verkettung und Verschachtelung von Befehls-Objekten erstellen.

Die Befehle werden von einem separaten Objekt ausgeführt, dem Aufrufer . Der Vorteil der Verwendung des Befehlsmusters (anstelle der Ausführung einer Reihe gewöhnlicher Anweisungen) besteht darin, dass verschiedene Aufrufer unterschiedliche Logiken für die Ausführung der Befehle anwenden können.

Das Befehlsmuster kann verwendet werden, um Sprachfunktionen hinzuzufügen (oder zu entfernen), die von der Hostsprache nicht unterstützt werden. Zum Beispiel könnte man in einer hypothetischen OO-Sprache ohne Ausnahmen eine Ausnahmesemantik hinzufügen, indem man den Befehlen "try"- und "throw"-Methoden zur Verfügung stellt. Wenn ein Befehl throw aufruft, geht der Aufrufer die Liste (oder den Baum) der Befehle bis zum letzten "try"-Aufruf zurück. Umgekehrt könnte man die Ausnahmesemantik aus einer Sprache entfernen (wenn man glaubt Ausnahmen sind schlecht ), indem alle Ausnahmen, die von den einzelnen Befehlen ausgelöst werden, abgefangen und in Fehlercodes umgewandelt werden, die dann an den nächsten Befehl weitergegeben werden.

Sogar ausgefallenere Ausführungssemantiken wie Transaktionen, nichtdeterministische Ausführung oder Fortsetzungen können auf diese Weise in einer Sprache implementiert werden, die sie nicht von Haus aus unterstützt. Es ist ein ziemlich mächtiges Muster, wenn Sie darüber nachdenken.

In der Realität werden die Befehlsmuster jedoch nicht als allgemeines Sprachmerkmal verwendet. Der Overhead, jede Anweisung in eine eigene Klasse zu verwandeln, würde zu einer unerträglichen Menge an Boilerplate-Code führen. Aber im Prinzip kann man damit die gleichen Probleme lösen, wie sie in fp mit Monaden gelöst werden.

30 Stimmen

Ich glaube, dies ist die erste Monaden-Erklärung, die ich gesehen habe, die sich nicht auf Konzepte der funktionalen Programmierung stützt und sie in echten OOP-Begriffen ausdrückt. Eine wirklich gute Antwort.

2 Stimmen

Dies ist sehr nahe an dem, was Monaden in FP/Haskell sind, außer dass die Befehlsobjekte selbst "wissen", zu welcher "Aufruflogik" sie gehören (und nur die kompatiblen können miteinander verkettet werden); invoker liefert nur den ersten Wert. Es ist nicht so, dass der Befehl "Drucken" von einer "nicht-deterministischen Ausführungslogik" ausgeführt werden kann. Nein, es muss eine "E/A-Logik" sein (d.h. eine IO-Monade). Aber ansonsten ist es sehr nahe dran. Man könnte auch einfach sagen, dass Monaden sind nur Programme (Aufbau von Code-Anweisungen, die später ausgeführt werden sollen). In den frühen Tagen wurde von "bind" gesprochen als "programmierbares Semikolon" .

5 Stimmen

@DavidK.Hess Ich bin in der Tat unglaublich skeptisch gegenüber Antworten, die FP verwenden, um grundlegende FP-Konzepte zu erklären, und insbesondere Antworten, die eine FP-Sprache wie Scala verwenden. Gut gemacht, JacquesB!

66voto

BMeph Punkte 1462

In Begriffen, die ein OOP-Programmierer verwenden würde verstehen würde (ohne funktionalen Hintergrund der funktionalen Programmierung), was ist eine Monade?

Welches Problem wird damit gelöst und welche sind die häufigsten Einsatzorte? sind die häufigsten Einsatzorte?

Im Sinne der OO-Programmierung ist eine Monade eine Schnittstelle (oder eher ein Mixin), die durch einen Typ parametrisiert ist und zwei Methoden hat, return y bind die beschreiben:

  • Wie man einen Wert injiziert, um einen monadischen Wert dieses injizierten Wertes Typ;
  • Wie kann man eine Funktion verwenden, die einen monadischen Wert aus einem nicht monadischen Wert macht, auf einen monadischen Wert.

Das Problem, das damit gelöst wird, ist das gleiche, das man von jeder Schnittstelle erwartet, nämlich, "Ich habe einen Haufen verschiedener Klassen, die unterschiedliche Dinge tun, aber diese unterschiedlichen Dinge auf eine Art und Weise tun, die eine grundlegende Ähnlichkeit aufweist. Wie kann ich diese Ähnlichkeit zwischen ihnen beschreiben, auch wenn die Klassen selbst nicht wirklich Subtypen von etwas Genauerem als der Klasse 'Object' selbst sind?"

Genauer gesagt, die Monad "Schnittstelle" ist vergleichbar mit IEnumerator o IIterator da es einen Typ annimmt, der selbst einen Typ annimmt. Der wichtigste "Punkt" von Monad ist jedoch die Möglichkeit, Operationen auf der Grundlage des inneren Typs zu verbinden, sogar bis hin zu einem neuen "internen Typ", wobei die Informationsstruktur der Hauptklasse beibehalten oder sogar verbessert wird.

1 Stimmen

return wäre eigentlich keine Methode der Monade, da sie keine Monadeninstanz als Argument annimmt. (d.h.: es gibt kein this/self)

1 Stimmen

@LaurenceGonsalves: Da ich mich gerade für meine Bachelorarbeit damit beschäftige, denke ich, dass das Fehlen von statischen Methoden in Interfaces in C#/Java die größte Einschränkung darstellt. Man könnte einen weiten Weg in die Richtung gehen, die ganze Monaden-Geschichte zu implementieren, zumindest statisch gebunden statt basierend auf Typklassen. Interessanterweise würde dies sogar trotz des Fehlens von Typen mit höherem Typ funktionieren.

44voto

Dmitri Zaitsev Punkte 12685

Um schnelle Leser zu respektieren, beginne ich mit einer präzisen Definition, fahre dann mit einer kurzen Erklärung auf "einfachem Englisch" fort und gehe dann zu den Beispielen über.

Hier ist eine kurze und präzise Definition leicht umformuliert:

A Monade (in der Informatik) ist formell eine Karte, die:

  • sendet jede Art X einer bestimmten Programmiersprache auf einen neuen Typ T(X) (genannt die "Art der T -Rechnungen mit Werten in X ");

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

  • auf eine Weise, die im offensichtlichen Sinne assoziativ und in Bezug auf eine gegebene Einheitsfunktion namens pure_X:X->T(X) ist als Übergabe eines Wertes an die reine Berechnung zu verstehen, die diesen Wert einfach zurückgibt.

Mit einfachen Worten: ein Monade es un Regel von jedem Typ zu übergeben X zu einer anderen Art T(X) und eine Regel zur Übergabe von zwei Funktionen f:X->T(Y) y g:Y->T(Z) (die Sie gerne komponieren würden, aber nicht können) in eine neue Funktion h:X->T(Z) . Die jedoch, ist nicht die Zusammensetzung im streng mathematischen Sinne. Wir "verbiegen" im Grunde die Zusammensetzung von Funktionen oder definieren neu, wie Funktionen zusammengesetzt sind.

Außerdem muss die Kompositionsregel der Monade die "offensichtlichen" mathematischen Axiome erfüllen:

  • Assoziativität : Komponieren f con g und dann mit h (von außen) sollte dasselbe sein wie das Zusammenstellen von g con h und dann mit f (von innen).
  • Unbewegliches Vermögen : Komponieren f mit dem Identität Funktion auf beiden Seiten sollte ergeben f .

Um es noch einmal einfach auszudrücken: Wir können unsere Funktionszusammensetzung nicht nach Belieben umdefinieren:

  • Wir brauchen zunächst die Assoziativität, um mehrere Funktionen in einer Reihe zusammenstellen zu können, z. B. f(g(h(k(x))) und sich nicht um die Angabe der Reihenfolge zu kümmern, in der die Funktionspaare gebildet werden. Da die Monadenregel nur vorschreibt, wie man eine Funktionspaar Ohne dieses Axiom müssten wir wissen, welches Paar zuerst zusammengesetzt wird, und so weiter. (Man beachte, dass dies etwas anderes ist als die Eigenschaft der Kommutativität, die f komponiert mit g waren die gleichen wie g komponiert mit f was nicht erforderlich ist).
  • Und zweitens brauchen wir die Unital-Eigenschaft, die einfach besagt, dass sich Identitäten trivialerweise so zusammensetzen, wie wir es erwarten. So können wir Funktionen sicher refaktorisieren, wenn diese Identitäten extrahiert werden können.

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

In der Praxis möchten Sie, dass die Monade für Sie von der Sprache, dem Compiler oder dem Framework implementiert wird, die sich um die Komposition von Funktionen für Sie kümmern würden. So können Sie sich auf das Schreiben der Logik Ihrer Funktionen konzentrieren und müssen sich nicht darum kümmern, wie deren Ausführung implementiert wird.

Das ist im Wesentlichen alles, kurz und bündig.


Als professioneller Mathematiker ziehe ich es vor, die Bezeichnung h die "Zusammensetzung" von f y g . Denn mathematisch gesehen ist sie es nicht. Die Bezeichnung "Zusammensetzung" setzt fälschlicherweise voraus, dass h die wahre mathematische Zusammensetzung ist, was sie nicht ist. Sie ist nicht einmal eindeutig bestimmt durch f y g . Stattdessen ist es das Ergebnis der neuen "Kompositionsregel" unserer Monade für die Funktionen. Diese kann sich völlig von der tatsächlichen mathematischen Zusammensetzung unterscheiden, selbst wenn diese existiert!


Um es weniger trocken zu machen, möchte ich versuchen, es an einem Beispiel zu verdeutlichen die ich mit kleinen Abschnitten kommentiere, so dass Sie gleich zur Sache kommen können.

Beispiele für das Auslösen von Ausnahmen als Monade

Angenommen, wir wollen zwei Funktionen zusammenstellen:

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

Aber f(0) ist nicht definiert, so dass eine Ausnahme e geworfen wird. Wie können Sie dann den Kompositionswert definieren g(f(0)) ? Natürlich wieder eine Ausnahme auslösen! Vielleicht die gleiche e . Vielleicht eine neue aktualisierte Ausnahme e1 .

Was passiert hier genau? Zunächst benötigen wir neue Ausnahmewerte (unterschiedlich oder gleich). Sie können sie aufrufen nothing o null oder was auch immer, aber das Wesentliche bleibt dasselbe - es sollten neue Werte sein, z.B. sollte es nicht ein number in unserem Beispiel hier. Ich ziehe es vor, sie nicht als null um Verwirrung zu vermeiden, wie null kann in jeder spezifischen Sprache implementiert werden. Ebenso ziehe ich es vor, zu vermeiden nothing weil sie häufig mit null was im Prinzip das ist, was null tun sollte, wird dieser Grundsatz jedoch oft aus irgendwelchen praktischen Gründen aufgeweicht.

Was genau ist eine Ausnahme?

Dies ist eine triviale Angelegenheit für jeden erfahrenen Programmierer, aber ich möchte ein paar Worte verlieren, um jeden Wurm der Verwirrung auszulöschen:

Exception ist ein Objekt, das Informationen darüber kapselt, wie das ungültige Ausführungsergebnis zustande kam.

Dies kann vom Wegwerfen aller Details bis zur Rückgabe eines einzigen globalen Wertes reichen (wie NaN o null ) oder eine lange Protokollliste zu erstellen oder was genau passiert ist, diese an eine Datenbank zu senden und über die gesamte verteilte Datenspeicherungsschicht zu replizieren ;)

Der wichtige Unterschied zwischen diesen beiden extremen Beispielen für Ausnahmen besteht darin, dass es im ersten Fall keine Nebeneffekte . In der zweiten gibt es. Das bringt uns zu der (Tausend-Dollar-)Frage:

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 sein. Wir ändern also unsere Funktion f durch Senden 0 auf den neuen abstrakten Wert e die wir als Ausnahme bezeichnen. Wir stellen sicher, dass der Wert e keine externen Informationen enthält, die nicht eindeutig durch unsere Eingabe bestimmt sind, d. h. x . Hier ist also 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'
}

Eigentlich hat sie nur dann Nebenwirkungen, wenn sich diese Nachricht in Zukunft möglicherweise ändern kann. Wenn aber garantiert ist, dass sie sich nie ändert, wird dieser Wert eindeutig vorhersehbar und hat somit keine Nebenwirkung.

Um es noch dümmer zu machen. Eine Funktion, die 42 ist eindeutig rein. Aber wenn jemand Verrücktes beschließt, die 42 eine Variable, deren Wert sich ändern könnte, so ist dieselbe Funktion unter den neuen Bedingungen nicht mehr rein.

Beachten Sie, dass ich der Einfachheit halber die Objektliteralschreibweise verwende, um das Wesentliche zu verdeutlichen. Leider sind die Dinge in Sprachen wie JavaScript, wo error ist kein Typ, der sich in Bezug auf die Funktionskomposition so verhält, wie wir es hier wollen, während tatsächliche Typen wie null o NaN verhalten sich nicht auf diese Weise, sondern durchlaufen einige künstliche und nicht immer intuitive Typumwandlungen.

Typ Erweiterung

Da wir die Nachricht innerhalb unserer Ausnahme variieren wollen, deklarieren wir eigentlich einen neuen Typ E für das gesamte Ausnahmeobjekt und dann Das ist es, was die maybe number tut, abgesehen von seinem verwirrenden Namen, der entweder vom Typ number oder des neuen Ausnahmetyps E also ist es wirklich die Gewerkschaft number | E de number y E . Insbesondere hängt es davon ab, wie wir Folgendes konstruieren wollen E die im Namen weder angedeutet noch reflektiert wird maybe number .

Was ist eine funktionelle Zusammensetzung?

Es ist die mathematische Operation, die Funktionen f: X -> Y y g: Y -> Z und konstruieren ihre Zusammensetzung als Funktion h: X -> Z zufriedenstellend h(x) = g(f(x)) . Das Problem mit dieser Definition tritt auf, wenn das Ergebnis f(x) ist nicht erlaubt als Argument von g .

In der Mathematik können diese Funktionen nicht ohne zusätzliche Arbeit zusammengesetzt werden. Die streng mathematische Lösung für unser obiges Beispiel von f y g ist zu entfernen 0 aus der Menge der Definitionen von f . Mit diesem neuen Satz von Definitionen (neue restriktivere Art von x ), f wird zusammensetzbar mit g .

In der Programmierung ist es jedoch nicht sehr praktisch, die Definitionsmenge von f wie diese. Stattdessen können Ausnahmen verwendet werden.

Oder es werden künstliche Werte geschaffen, wie NaN , undefined , null , Infinity usw. Sie bewerten also 1/0 à Infinity y 1/-0 à -Infinity . Und erzwingen Sie dann den neuen Wert zurück in Ihren Ausdruck, anstatt eine Ausnahme zu machen. Das kann zu Ergebnissen führen, die Sie vorhersehbar finden oder auch nicht:

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

Und schon sind wir wieder bei den normalen Zahlen und können weitermachen ;)

JavaScript erlaubt es uns, numerische Ausdrücke um jeden Preis auszuführen, ohne Fehler wie im obigen Beispiel zu machen. Das heißt, es erlaubt auch, Funktionen zu komponieren. Und genau darum geht es bei der Monade - sie ist eine Regel für die Zusammenstellung von Funktionen, die die zu Beginn dieser Antwort definierten Axiome erfüllen.

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

Zur Beantwortung dieser Frage genügt es, die Axiome zu überprüfen (als Übung belassen, da sie hier nicht Teil der Frage sind).

Kann das Auslösen einer Ausnahme zur Konstruktion einer Monade verwendet werden?

Eine nützlichere Monade wäre stattdessen die Regel, die vorschreibt dass, wenn f wirft eine Ausnahme für einige x so auch seine Zusammensetzung mit einem beliebigen g . Und machen Sie die Ausnahme E global eindeutig mit nur einem möglichen Wert ( Terminalobjekt in der Kategorientheorie). Jetzt sind die beiden Axiome sofort überprüfbar und wir erhalten eine sehr nützliche Monade. Und das Ergebnis ist das, was bekannt ist als die vielleicht Monade .

4 Stimmen

Guter Beitrag. +1 Aber vielleicht sollten Sie "die meisten Erklärungen sind zu lang ..." streichen, da Ihre die längste ist. Andere werden beurteilen, ob es "Klartext" ist, wie es die Frage verlangt: "plain english == in einfachen Worten, auf einfache Weise".

1 Stimmen

@cibercitizen1 Danke! Es ist tatsächlich kurz, wenn man das Beispiel nicht mitzählt. Der wichtigste Punkt ist, dass Sie Sie müssen das Beispiel nicht lesen, um die Definition zu verstehen. . Leider sind viele Erklärungen mich zwingen, zuerst Beispiele zu lesen Dies ist oft unnötig, kann aber natürlich zusätzliche Arbeit für den Verfasser bedeuten. Wenn man sich zu sehr auf konkrete Beispiele stützt, besteht die Gefahr, dass unwichtige Details das Bild verdunkeln und das Verständnis erschweren. Abgesehen davon haben Sie gute Argumente, siehe die Aktualisierung.

2 Stimmen

Zu lang und verwirrend

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X