Es ist ein Jahr her, seit ich diese Frage gestellt habe. Nachdem ich es veröffentlicht hatte, beschäftigte ich mich ein paar Monate lang mit Haskell. Ich habe es sehr genossen, aber ich habe es beiseite gelegt, als ich bereit war, mich mit Monaden zu beschäftigen. Ich machte mich wieder an die Arbeit und konzentrierte mich auf die Technologien, die mein Projekt benötigte.
Das ist ziemlich cool. Es ist allerdings etwas abstrakt. Ich kann mir vorstellen, dass Leute, die nicht wissen, welche Monaden bereits sind, aufgrund des Mangels an echten Beispielen verwirrt sind.
Lassen Sie mich versuchen, die Anforderungen zu erfüllen, und um ganz klar zu sein, mache ich ein Beispiel in C #, auch wenn es hässlich aussehen wird. Ich werde am Ende das entsprechende Haskell hinzufügen und Ihnen den coolen syntaktischen Haskell-Zucker zeigen, bei dem, IMO, Monaden wirklich nützlich werden.
Okay, eine der einfachsten Monaden heißt in Haskell "Vielleicht Monade". In C # wird der Typ Vielleicht aufgerufen Nullable<T>
. Es ist im Grunde eine winzige Klasse, die nur das Konzept eines Wertes kapselt, der entweder gültig ist und einen Wert hat oder "null" ist und keinen Wert hat.
Eine nützliche Sache, um in einer Monade zu bleiben, um Werte dieses Typs zu kombinieren, ist der Begriff des Versagens. Das heißt, wir möchten in der Lage sein, mehrere nullbare Werte zu betrachten und zurückzukehren null
, sobald einer von ihnen null ist. Dies kann nützlich sein, wenn Sie beispielsweise viele Schlüssel in einem Wörterbuch oder Ähnlichem nachschlagen und am Ende alle Ergebnisse verarbeiten und irgendwie kombinieren möchten. Wenn sich jedoch einer der Schlüssel nicht im Wörterbuch befindet, Sie wollen null
für die ganze Sache zurückkehren. Es wäre mühsam, jede Suche manuell überprüfen
null
und zurückgeben zu müssen, damit wir diese Überprüfung im Bindungsoperator verbergen können (was eine Art Punkt für Monaden ist, wir verstecken die Buchhaltung im Bindungsoperator, was den Code einfacher macht verwenden, da wir die Details vergessen können).
Hier ist das Programm, das das Ganze motiviert (ich werde das Bind
später definieren
, dies soll Ihnen nur zeigen, warum es schön ist).
class Program
{
static Nullable<int> f(){ return 4; }
static Nullable<int> g(){ return 7; }
static Nullable<int> h(){ return 9; }
static void Main(string[] args)
{
Nullable<int> z =
f().Bind( fval =>
g().Bind( gval =>
h().Bind( hval =>
new Nullable<int>( fval + gval + hval ))));
Console.WriteLine(
"z = {0}", z.HasValue ? z.Value.ToString() : "null" );
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
Ignorieren Sie jetzt für einen Moment, dass dies Nullable
in C # bereits unterstützt wird (Sie können nullfähige Ints zusammenfügen und erhalten null, wenn beides null ist). Stellen wir uns vor, es gibt keine solche Funktion und es handelt sich nur um eine benutzerdefinierte Klasse ohne besondere Magie. Der Punkt ist, dass wir die Bind
Funktion verwenden können, um eine Variable an den Inhalt unseres Nullable
Werts zu binden und dann so zu tun, als ob nichts Seltsames vor sich geht, und sie wie normale Ints verwenden und sie einfach addieren können. Wir wickeln das Ergebnis in einem nullable am Ende, und das NULL festlegbaren entweder null sein (wenn eine der f
, g
oder h
kehrt null) oder es wird das Ergebnis des Summierens sein f
, g
undh
zusammen. (Dies ist analog dazu, wie wir eine Zeile in einer Datenbank an eine Variable in LINQ binden und damit arbeiten können, sicher in dem Wissen, dass der Bind
Operator sicherstellen wird, dass der Variablen immer nur gültige Zeilenwerte übergeben werden).
Sie können damit spielen und jedes von f
, ändern g
und h
null zurückgeben, und Sie werden sehen, dass das Ganze null zurückgibt.
Daher muss der Bind-Operator diese Überprüfung für uns durchführen und die Rückgabe von Null retten, wenn er auf einen Null-Wert stößt, und ansonsten den Wert innerhalb der Nullable
Struktur an das Lambda weitergeben.
Hier ist der Bind
Operator:
public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f )
where B : struct
where A : struct
{
return a.HasValue ? f(a.Value) : null;
}
Die Typen hier sind genau wie im Video. Es benötigt eine M a
( Nullable<A>
in diesem Fall in C # -Syntax) und eine Funktion von a
bis
M b
( Func<A, Nullable<B>>
in C # -Syntax) und gibt eine M b
( Nullable<B>
) zurück.
Der Code prüft einfach, ob die Null-Datei einen Wert enthält, extrahiert ihn in diesem Fall und übergibt ihn an die Funktion. Andernfalls wird nur Null zurückgegeben. Dies bedeutet, dass der Bind
Operator die gesamte Nullprüflogik für uns übernimmt. Wenn und nur wenn der Wert, den wir aufrufen,
Bind
nicht null ist, wird dieser Wert an die Lambda-Funktion "weitergegeben", andernfalls werden wir frühzeitig aussteigen und der gesamte Ausdruck ist null. Dies ermöglicht es den Code , dass wir die Monade Schreib mit ganz diesem Null-Kontrolle Verhalten frei zu sein, wir verwenden nur Bind
und eine Variable auf den Wert innerhalb des monadischen Wertes gebunden ( fval
,
gval
und hval
in dem Beispiel - Code) und wir können sie sicher nutzen in dem Wissen, Bind
das sich darum kümmert, sie auf Null zu prüfen, bevor sie weitergegeben werden.
Es gibt andere Beispiele für Dinge, die Sie mit einer Monade tun können. Beispielsweise können Sie den Bind
Operator veranlassen, sich um einen Eingabestrom von Zeichen zu kümmern und damit Parser-Kombinatoren zu schreiben. Jeder Parser-Kombinator kann dann Dinge wie Back-Tracking, Parser-Fehler usw. völlig ignorieren und einfach kleinere Parser miteinander kombinieren, als ob niemals etwas schief gehen würde, sicher in dem Wissen, dass eine clevere Implementierung Bind
die gesamte Logik hinter dem aussortiert schwierige Teile. Später fügt vielleicht jemand der Monade eine Protokollierung hinzu, aber der Code, der die Monade verwendet, ändert sich nicht, da die gesamte Magie in der Definition des Bind
Operators geschieht und der Rest des Codes unverändert bleibt.
Schließlich ist hier die Implementierung des gleichen Codes in Haskell ( --
beginnt eine Kommentarzeile).
-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a
-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x
-- the "unit", called "return"
return = Just
-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
g >>= ( \gval ->
h >>= ( \hval -> return (fval+gval+hval ) ) ) )
-- The following is exactly the same as the three lines above
z2 = do
fval <- f
gval <- g
hval <- h
return (fval+gval+hval)
Wie Sie sehen können, sieht die schöne do
Notation am Ende wie ein gerader Imperativcode aus. Und in der Tat ist dies beabsichtigt. Monaden können verwendet werden, um alle nützlichen Dinge in der imperativen Programmierung (veränderlichen Zustand, E / A usw.) zu kapseln und mit dieser netten imperativartigen Syntax zu verwenden, aber hinter den Vorhängen sind alles nur Monaden und eine clevere Implementierung des Bind-Operators! Das Coole ist, dass Sie Ihre eigenen Monaden implementieren können, indem Sie >>=
und implementieren return
. Und wenn Sie dies tun, können diese Monaden auch die do
Notation verwenden, was bedeutet, dass Sie im Grunde genommen Ihre eigenen kleinen Sprachen schreiben können, indem Sie nur zwei Funktionen definieren!