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)
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