2014-01-26 18 views
13

viel Zeit Lesen und Denken Nachdem er, ich glaube, ich habe endlich begriffen, was Monaden sind, wie sie funktionieren und was sie für nützlich sind. Mein Hauptziel war herauszufinden, ob Monaden etwas sind, was ich für meine tägliche Arbeit in C# anwenden könnte.hat ein IO-Monade Sinn in einer Sprache wie C#

Wenn ich über Monaden begann das Lernen, hatte ich den Eindruck, dass sie magisch sind, und dass sie irgendwie machen IO und andere nicht reine Funktionen rein.

verstehe ich die Bedeutung von Monaden für Dinge wie LINQ in .Net, und vielleicht ist sehr nützlich für die mit Funktionen handelt, die gültigen Werte nicht zurückgeben. Und ich schätze auch die Notwendigkeit, die Anfälligkeit im Code zu begrenzen und externe Abhängigkeiten zu isolieren, und ich hatte gehofft, Monaden würden auch bei diesen helfen.

Aber ich bin schließlich zu der Schlussfolgerung gekommen, dass Monaden für IO und Handling State eine Notwendigkeit für Haskell sind, weil Haskell keine andere Möglichkeit hat, es zu tun (sonst könnte man Sequencing nicht garantieren, und einige Aufrufe wären Optimiert away.) Aber für mehr Mainstream-Sprachen, Monaden sind nicht gut für diese Bedürfnisse, da die meisten Sprachen bereits behandeln und IO und Zustand einfach.

Also, meine Frage ist, ist es fair zu sagen, dass die IO Monade wirklich in Haskell nur dann sinnvoll ist? Gibt es einen guten Grund, eine IO-Monade in, sagen wir, C# zu implementieren?

+0

Nein, ich denke nicht, da jede Funktion frei ist, IO zu tun und Sie müssten extrem diszipliniert sein, um sicherzustellen, dass Sie IO nicht außerhalb der Monade in Ihrem eigenen Code machen und externen Code als markieren in IO sein. – Lee

Antwort

12

ich Haskell und F # regelmäßig und ich habe einen IO oder Zustand Monade in F # wie mit nie wirklich gefühlt.

Der Haupt für mich Grund ist, dass in Haskell, die Sie von der Art der etwas sagen kann, dass es nicht Verwendung IO oder Zustand, und das ist eine wirklich wertvolle Information.

In F # (und C#) gibt es keine allgemeine Erwartung für den Code anderer Leute, und daher profitieren Sie nicht viel davon, dass Sie Ihrem eigenen Code diese Disziplin hinzufügen, und Sie zahlen einen allgemeinen Overhead (hauptsächlich syntaktisch) dranbleiben.

Monaden funktionieren auch nicht gut auf der .NET-Plattform wegen des Fehlens von : während Sie monadischen Code in F # mit Workflow-Syntax schreiben können, und in C# mit ein bisschen mehr Schmerzen, können Sie nicht einfach schreibe Code, der über mehrere verschiedene Monaden abstrahiert.

4

Die Fähigkeit zu wissen, ob eine Funktion Nebenwirkungen nur durch auf seiner Unterschrift sucht, ist sehr nützlich, wenn versucht zu verstehen, was die Funktion tut. Je weniger eine Funktion kann tun, desto weniger müssen Sie verstehen! (. Polymorphismus eine andere Sache ist, dass beschränken hilft, was eine Funktion mit seinen Argumenten tun)

In vielen Sprachen, die Software Transactional Memory implementieren, die Dokumentation hat warnings like the following:

I/O und andere Aktivitäten mit Seiten -Effekte sollten in Transaktionen vermieden werden, da Transaktionen erneut versucht werden.

Wenn diese Warnung zu einem vom Typsystem erzwungenen Verbot wird, kann die Sprache sicherer werden.

Es gibt Optimierungen, die nur mit Code durchgeführt werden können, der frei von Nebenwirkungen ist. Aber das Fehlen von Nebenwirkungen kann schwierig zu bestimmen sein, wenn Sie überhaupt "alles erlauben".

Ein weiterer Vorteil der IO-Monade ist, dass IO-Aktionen "inert" sind, sofern sie nicht im Pfad der main-Funktion liegen, sie als Daten zu manipulieren, in Container zu setzen, zur Laufzeit zu erstellen und bald.

Natürlich hat der monadische Ansatz zu IO seine Nachteile. Aber es hat Vorteile neben "einer der wenigen Möglichkeiten, I/O in einer reinen faulen Sprache in einer flexiblen und prinzipientreuen Weise zu tun".

7

Sie fragen "Brauchen wir eine IO-Monade in C#?" aber Sie sollten stattdessen fragen: "Brauchen wir einen Weg, Reinheit und Unveränderlichkeit in C# zuverlässig zu erhalten?".

Der Hauptvorteil wäre die Kontrolle von Nebenwirkungen. Ob Sie das mit Monaden oder einem anderen Mechanismus tun, spielt keine Rolle. Mit C# können Sie beispielsweise Methoden wie pure und Klassen wie immutable markieren. Das würde sehr gut dazu beitragen, Nebenwirkungen zu zähmen.

In solch einer hypothetischen Version von C# würden Sie versuchen, 90% der Berechnung rein zu machen, und haben uneingeschränkte, eifrige IO und Nebenwirkungen in den restlichen 10%. In einer solchen Welt sehe ich nicht so sehr ein Bedürfnis nach absoluter Reinheit und einer IO-Monade.

Beachten Sie, dass Sie durch einfaches mechanisches Konvertieren von seitenbeeinflussendem Code in einen monadischen Stil nichts gewinnen. Der Code verbessert die Qualität überhaupt nicht. Sie verbessern die Codequalität, indem Sie 90% rein sind und das IO in kleine, leicht überprüfbare Stellen konzentrieren.

1

In einer Sprache wie C#, wo Sie IO überall machen können, hat eine IO-Monade keinen praktischen Nutzen. Das einzige, wofür Sie es verwenden möchten, ist die Kontrolle von Nebenwirkungen, und da Sie nichts davon abhalten können, Nebenwirkungen außerhalb der Monade auszuführen, gibt es nicht viel Sinn.

Wie für die Maybe Monade, während es potenziell nützlich scheint, funktioniert es nur wirklich in einer Sprache mit fauler Bewertung. Im folgenden Haskell Expression wird das zweite lookup nicht, wenn die ersten Erträge Nothing ausgewertet:

doSomething :: String -> Maybe Int 
doSomething name = do 
    x <- lookup name mapA 
    y <- lookup name mapB 
    return (x+y) 

Diese den Ausdruck „Schaltung kurz“ zu können, wenn ein Nothing angetroffen wird. Eine Implementierung in C# müsste beide Nachschlagevorgänge durchführen (ich denke, ich wäre an einem Gegenbeispiel interessiert). Mit if-Anweisungen sind Sie wahrscheinlich besser dran.

Ein anderes Problem ist der Verlust der Abstraktion. Während es natürlich möglich ist, Monaden in C# zu implementieren (oder Dinge, die ein wenig wie Monaden aussehen), kann man nicht wirklich verallgemeinern wie in Haskell, weil C# keine höheren Arten hat. Zum Beispiel kann eine Funktion wie mapM :: Monad m => Monad m => (a -> m b) -> [a] -> m [b] (die für beliebige Monade funktioniert) nicht wirklich in C# dargestellt werden. Man könnte sicherlich etwas davon hat:

public List<Maybe<a> mapM<a,b>(Func<a, Maybe<b>>); 

, die für einen bestimmten Monade (Maybe in diesem Fall) funktionieren würde, aber es ist nicht möglich, zu abstrakt-away der Maybe von dieser Funktion. Sie müssten in der Lage sein, etwas in der Art zu tun:

public List<m<a> mapM<m,a,b>(Func<a, m<b>>); 

was in C# nicht möglich ist.

+2

Eine Implementierung von 'Maybe/Option' in C# erfordert die Implementierung von' bind' (d. H. 'SelectMany'), was einen 'Func >' akzeptiert, der langsam ausgewertet wird. F # ist eine strenge Sprache und hat kein Problem mit der Implementierung von Monaden. – Lee

+0

Mein Punkt über Faulheit war nicht, dass es erforderlich ist, Monaden zu implementieren, aber dass einige Monaden (ich denke, Maybe und Entweder insbesondere) sind nicht wirklich nützlich in einer strengen Sprache –

+2

Das Argument zu 'bind' _ist_ bewertet träge, sogar in einer strengen Sprache. "Option" wird häufig in F # verwendet und wäre in C# nützlich, außer dass es durch die verschiedenen Arten der Darstellung von "null" in der Sprache kompliziert ist. Gleiches gilt für "Entweder". – Lee

14

Bei der Arbeit verwenden wir Monaden, um IO in unserem C# -Code auf unseren wichtigsten Geschäftslogiken zu steuern.Zwei Beispiele sind unser Finanzcode und Code, der Lösungen für ein Optimierungsproblem für unsere Kunden findet.

In unserem Finanz-Code verwenden wir eine Monade IO und aus unserer Datenbank zu lesen Schreiben zu steuern. Es besteht im Wesentlichen aus einer kleinen Menge von Operationen und einem abstrakten Syntaxbaum für die Monad-Operationen. Man könnte sich vorstellen, es ist so etwas wie dieses (nicht unbedingt Code):

interface IFinancialOperationVisitor<T, out R> : IMonadicActionVisitor<T, R> { 
    R GetTransactions(GetTransactions op); 
    R PostTransaction(PostTransaction op); 
} 

interface IFinancialOperation<T> { 
    R Accept<R>(IFinancialOperationVisitor<T, R> visitor); 
} 

class GetTransactions : IFinancialOperation<IError<IEnumerable<Transaction>>> { 
    Account Account {get; set;}; 

    public R Accept<R>(IFinancialOperationVisitor<R> visitor) { 
     return visitor.Accept(this); 
    } 
} 

class PostTransaction : IFinancialOperation<IError<Unit>> { 
    Transaction Transaction {get; set;}; 

    public R Accept<R>(IFinancialOperationVisitor<R> visitor) { 
     return visitor.Accept(this); 
    } 
} 

, die im Wesentlichen Code der Haskell ist

data FinancialOperation a where 
    GetTransactions :: Account -> FinancialOperation (Either Error [Transaction]) 
    PostTransaction :: Transaction -> FinancialOperation (Either Error Unit) 

zusammen mit einem abstrakten Syntaxbaum für den Bau von Aktionen in einer Monade, im Wesentlichen die kostenlos Monade:

interface IMonadicActionVisitor<in T, out R> { 
    R Return(T value); 
    R Bind<TIn>(IMonadicAction<TIn> input, Func<TIn, IMonadicAction<T>> projection); 
    R Fail(Errors errors); 
}  

// Objects to remember the arguments, and pass them to the visitor, just like above 

// Hopefully I got the variance right on everything for doing this without higher order types, which is how we used to do this. We now use higher order types in c#, more on that below. Here, to avoid a higher-order type, the AST for monadic actions is included by inheritance in 

In dem realen Code, gibt es mehr von diesen so können wir uns daran erinnern, dass etwas von .Select() gebaut wurde statt .SelectMany() für Effizienz. Eine Finanzoperation, einschließlich Zwischenberechnungen, hat immer noch den Typ IFinancialOperation<T>. Die tatsächliche Leistung der Operationen wird von einem Dolmetscher durchgeführt, die alle Datenbankoperationen in einer Transaktion wickelt und befasst sich damit, wie diese Transaktion zurück zu rollen, wenn eine Komponente nicht erfolgreich ist. Wir verwenden auch einen Interpreter, um den Code zu testen.

In unserem Optimierungscode verwenden wir eine Monade IO zur Steuerung externer Daten für die Optimierung zu erhalten. Dies ermöglicht es uns, den Code zu schreiben, der unwissend ist, wie Berechnungen zusammengesetzt sind, die wir genau das gleiche Geschäft Code in mehreren Einstellungen verwenden kann:

  • synchrone IO und Berechnungen für auf Anfrage getan Berechnungen
  • asynchrone IO und Berechnungen zum parallelen
  • verspottet IO getan viele Berechnungen für Unit-Tests

Da der Code übergeben werden muss, die Monade zu verwenden, müssen wir eine explizite Definition einer Monade. Hier ist eins. IEncapsulated<TClass,T> bedeutet im Wesentlichen TClass<T>. Auf diese Weise kann der C# -Compiler den Überblick über alle drei Stücke von der Art der Monaden gleichzeitig halten, wodurch die Notwendigkeit der Überwindung zu werfen, wenn sie mit Monaden selbst zu tun.

public interface IEncapsulated<TClass,out T> 
{ 
    TClass Class { get; } 
} 

public interface IFunctor<F> where F : IFunctor<F> 
{ 
    // Map 
    IEncapsulated<F, B> Select<A, B>(IEncapsulated<F, A> initial, Func<A, B> projection); 
} 

public interface IApplicativeFunctor<F> : IFunctor<F> where F : IApplicativeFunctor<F> 
{ 
    // Return/Pure 
    IEncapsulated<F, A> Return<A>(A value); 
    IEncapsulated<F, B> Apply<A, B>(IEncapsulated<F, Func<A, B>> projection, IEncapsulated<F, A> initial); 
} 

public interface IMonad<M> : IApplicativeFunctor<M> where M : IMonad<M> 
{ 
    // Bind 
    IEncapsulated<M, B> SelectMany<A, B>(IEncapsulated<M, A> initial, Func<A, IEncapsulated<M, B>> binding); 
    // Bind and project 
    IEncapsulated<M, C> SelectMany<A, B, C>(IEncapsulated<M, A> initial, Func<A, IEncapsulated<M, B>> binding, Func<A, B, C> projection); 
} 

public interface IMonadFail<M,TError> : IMonad<M> { 
    // Fail 
    IEncapsulated<M, A> Fail<A>(TError error); 
} 

Jetzt konnten wir machen eine weitere Klasse von Monade vorstellen, für den Teil der IO unsere Berechnungen müssen sehen, in der Lage sein:

public interface IMonadGetSomething<M> : IMonadFail<Error> { 
    IEncapsulated<M, Something> GetSomething(); 
} 

Dann können wir Code schreiben, der nicht weiß, wie Berechnungen zusammengesetzt werden

public class Computations { 

    public IEncapsulated<M, IEnumerable<Something>> GetSomethings<M>(IMonadGetSomething<M> monad, int number) { 
     var result = monad.Return(Enumerable.Empty<Something>()); 
     // Our developers might still like writing imperative code 
     for (int i = 0; i < number; i++) { 
      result = from existing in r1 
        from something in monad.GetSomething() 
        select r1.Concat(new []{something}); 
     } 
     return result.Select(x => x.ToList()); 
    } 
} 

Dies kann sowohl in einer ein IMonadGetSomething<> synchronen und asynchronen Implementierung wiederverwendet werden. Beachten Sie, dass in diesem Code die GetSomething() s nacheinander passieren wird, bis ein Fehler vorliegt, auch in einer asynchronen Einstellung. (Nein, das ist nicht, wie wir Listen im wirklichen Leben bauen)

+1

Ich liebe es, das sollte wirklich akzeptiert werden Antwort, aber es fehlen weitere Beispiele für die Verwendung Ihrer monadischen Bibliothek. – vittore

2

Wie immer ist die IO-Monade speziell und schwierig zu begründen. Es ist in der Haskell-Community gut bekannt, dass IO zwar nützlich ist, aber nicht viele der Vorteile, die andere Monaden haben. Es ist, wie Sie bemerkt haben, sehr motiviert durch seine Privilegienposition, anstatt es ein gutes Modellierungswerkzeug zu sein.

Damit würde ich sagen, es ist nicht so nützlich in C# oder wirklich jede Sprache, die nicht versucht, Nebenwirkungen mit Typ Anmerkungen vollständig zu enthalten.

Aber es ist nur eine Monade. Wie Sie bereits erwähnt haben, wird in LINQ Failure angezeigt, aber komplexere Monaden sind sogar in einer Nebeneffekt-Sprache nützlich.

Zum Beispiel, sogar mit willkürlichen globalen und lokalen staatlichen Milieus, wird die Staatsmonade sowohl den Anfang als auch das Ende eines Regimes von Aktionen anzeigen, die an einem privilegierten Staat arbeiten. Du erhältst die Nebeneffekte, die Haskell genießt, nicht, aber du bekommst immer noch eine gute Dokumentation.

Um weiter zu gehen, ist die Einführung von etwas wie eine Parser Monade ein Lieblingsbeispiel von mir. Diese monad ist sogar in C# eine gute Möglichkeit, Dinge wie nicht deterministische Fehler beim Zurückverfolgen zu lokalisieren, die während des Verzehrs einer Zeichenfolge ausgeführt werden. Sie können dies natürlich mit bestimmten Arten von Veränderlichkeit tun, aber Monaden drücken aus, dass ein bestimmter Ausdruck in diesem wirkungsvollen Regime eine nützliche Aktion ausführt, ohne auf irgendeinen globalen Zustand einzugehen, an dem Sie möglicherweise beteiligt sind.

Also würde ich ja sagen, sie sind in jeder getippten Sprache nützlich. Aber IO wie Haskell es tut? Vielleicht nicht so sehr.

+1

In welchem ​​Sinn ist die IO-Monade besonders und schwierig zu begründen? Sie gehorcht den Monadengesetzen, hat eine einfache Interpretation (Datenstruktur, die ein Imperativprogramm darstellt) und wenn Sie sich von unsicheren (alles 'unsichere * ';' Debug.Trace'; ...) Funktionen fernhalten (oder sie richtig verwenden)) Es bricht keine Nicht-IO-Teile des Programms. – delnan

+0

Es ist definitiv eine Monade, aber die Bezeichnung ist so düster wie ein normales imperatives Programm. Wenn du diese Trübung in C# willst, dann gewinnst du nichts. Andere Monaden haben jedoch eine sehr, sehr klare Bezeichnung a. –

Verwandte Themen