"Reader" -Monade durch Async / Warten in C #


In meinem vorherigen Artikel habe ich beschrieben, wie das "Vielleicht" -Monadenverhalten mit asynchronen / wartenden Operatoren erreicht wird. Dieses Mal werde ich zeigen, wie ein anderes beliebtes Designmuster "Reader Monad" mit denselben Techniken implementiert wird.


Dieses Muster ermöglicht die implizite Übergabe eines Kontexts an eine Funktion ohne Verwendung von Funktionsparametern oder gemeinsam genutzten globalen Objekten und kann als eine weitere Möglichkeit zur Implementierung der Abhängigkeitsinjektion angesehen werden. Zum Beispiel:


class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"})); //(Hi, John!, Hi, Jose!) Console.WriteLine(await GreetGuys().Apply(new Config {Template = "¡Hola, {0}!" })); //(¡Hola, John!, ¡Hola, Jose!) } //These functions do not have any link to any instance of the Config class. public static async Reader<(string gJohn, string gJose)> GreetGuys() => (await Greet("John"), await Greet("Jose")); static async Reader<string> Greet(string name) => string.Format(await ExtractTemplate(), name); static async Reader<string> ExtractTemplate() => await Reader<string>.Read<Config>(c => c.Template); 

Klassischer "Reader"


Schauen wir uns zunächst an, wie die Monade ohne asynchrone / wartende Operatoren implementiert werden kann:


 public class Config { public string Template; } public static class ClassicReader { public static void Main() { var greeter = GreetGuys(); Console.WriteLine(greeter.Apply(new Config{Template = "Hello, {0}"})); //(Hello, John, Hello, Jose) Console.WriteLine(greeter.Apply(new Config{Template = "¡Hola, {0}!" })); //(¡Hola, John!, ¡Hola, Jose!) } public static Reader<(string gJohn, string gJose), Config> GreetGuys() => from toJohn in Greet("John") from toJose in Greet("Jose") select (toJohn, toJose); //Without using the query notation the code would look like this: //Greet("John") // .SelectMany( // toJohn => Greet("Jose"), // (toJohn, toJose) => (toJohn, toJose)) public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name)); } 

(Leser)


 public class Reader<T, TCtx> { private readonly Func<TCtx, T> _exec; public Reader(Func<TCtx, T> exec) => this._exec = exec; public T Apply(TCtx ctx) => this._exec(ctx); } public static class Reader { public static Reader<TJoin, TCtx> SelectMany<TIn, TOut, TCtx, TJoin>( this Reader<TIn, TCtx> source, Func<TIn, Reader<TOut, TCtx>> bind, Func<TIn, TOut, TJoin> join) => new Reader<TJoin, TCtx>(ctx => { var inValue = source.Apply(ctx); var outValue = bind(inValue).Apply(ctx); return join(inValue, outValue); }); } 

Der Code funktioniert, sieht aber für C # -Entwickler nicht natürlich aus. Kein Wunder, denn Monaden stammten aus funktionalen Sprachen, in denen ein ähnlicher Code präziser geschrieben werden kann. Die klassische Implementierung hilft jedoch dabei, das Wesentliche des Musters zu verstehen. Statt Code sofort auszuführen, wird es in eine Funktion eingefügt, die aufgerufen wird, wenn der Kontext bereit ist.


 public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name)); //That is how the code would look like with explicit passing of context: //public static string Greet(string name, Config cfg) // => string.Format(cfg.Template, name); 

SelectMany kann mehrere solcher Funktionen zu einer einzigen kombinieren, sodass Sie eine Unterroutine erstellen können, deren Ausführung verzögert wird, bis der Kontext angewendet wird. Andererseits ähnelt dieser Ansatz dem Schreiben von asynchronem Code, bei dem die Programmausführung gestoppt wird, wenn eine asynchrone Operation ausgeführt wird. Wenn ein Ergebnis der Operation fertig ist, wird die Programmausführung fortgesetzt. Es wird davon ausgegangen, dass die C # -Infrastruktur, die für asynchrone Operationen ( async / await ) ausgelegt ist, bei der Implementierung der "Reader" -Monade irgendwie verwendet werden könnte, und ... die Annahme ist richtig!


Async "Reader"


In meinem vorherigen Artikel habe ich gezeigt, wie Sie mithilfe von generalisierten asynchronen Rückgabetypen die Kontrolle über asynchrone / wartende Operatoren erlangen. Diesmal wird die gleiche Technik angewendet. Beginnen wir mit der Klasse "Reader", die als Ergebnis für asynchrone Operationen verwendet wird:


 [AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ... 

Die Klasse hat zwei verschiedene Verantwortlichkeiten (theoretisch könnten wir 2 Klassen erstellen):


  1. Extrahieren eines Werts aus einem Kontext, wenn der Kontext angewendet wird
  2. Erstellen einer verknüpften Liste von Reader- Instanzen, mit denen ein Kontext über eine Aufrufhierarchie verteilt wird.

Für jede Verantwortung verwenden wir einen separaten Konstruktor:


 private readonly Func<object, T> _extractor; //1. Used to extract some value from a context public static Reader<T> Read<TCtx>(Func<TCtx, T> extractor) => new Reader<T>(ctx => extractor((TCtx)ctx)); private Reader(Func<object, T> exec) => this._extractor = exec; //2. Used by ReaderTaskMethodBuilder in a compiler generated code internal Reader() { } 

Wenn eine Instanz der Reader- Klasse als Argument des Operators await verwendet wird, erhält die Instanz einen Link zu einem Fortsetzungsdelegierten, der nur aufgerufen werden sollte, wenn ein Ausführungskontext aufgelöst wird, und wir können (aus dem Kontext) einige Daten extrahieren, die sein werden in der Fortsetzung verwendet.



Um Verbindungen zwischen übergeordneten und untergeordneten "Lesern" herzustellen, erstellen wir die folgende Methode:


 private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } } 

welches in ReaderTaskMethodBuilder aufgerufen wird :


 public class ReaderTaskMethodBuilder<T> { ... public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IReader reader) { this.Task.SetChild(reader); } awaiter.OnCompleted(stateMachine.MoveNext); } public Reader<T> Task { get; } } 

Innerhalb der SetChild- Methode rufen wir SetCtx auf, das einen Kontext bis zu einer Hierarchie weitergibt und einen Extraktor aufruft, wenn er definiert ist:


 public void SetCtx(object ctx) { this._ctx = ctx; if (this._ctx != null) { this._child?.SetCtx(this._ctx); if (this._extractor != null) { this.SetResult(this._extractor(this._ctx)); } } } 

SetResult speichert einen aus einem Kontext extrahierten Wert und ruft eine Fortsetzung auf:


 internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); } 

Wenn eine Reader- Instanz keinen initialisierten "Extraktor" hat, sollte SetResult von ReaderTaskMethodBuilder aufgerufen werden, wenn eine generierte Zustandsmaschine in ihren Endzustand wechselt.


Die Apply- Methode ruft nur SetCtx auf


 public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; } 

Sie finden den gesamten Code auf github ( falls er noch nicht blockiert ist )


Jetzt möchte ich ein realistischeres Beispiel zeigen, wie der asynchrone Reader verwendet werden kann:


Klicken Sie hier, um das Beispiel zu erweitern
 public static class ReaderTest { public class Configuration { public readonly int DataBaseId; public readonly string GreetingTemplate; public readonly string NameFormat; public Configuration(int dataBaseId, string greetingTemplate, string nameFormat) { this.DataBaseId = dataBaseId; this.GreetingTemplate = greetingTemplate; this.NameFormat = nameFormat; } } public static async Task Main() { int[] ids = { 1, 2, 3 }; Configuration[] configurations = { new Configuration(100, "Congratulations, {0}! You won {1}$!", "{0} {1}"), new Configuration(100, "¡Felicidades, {0}! Ganaste {1} $", "{0}"), }; foreach (var configuration in configurations) { foreach (var userId in ids) { //The logic receives only a single explicit parameter - userId var logic = GetGreeting(userId); //The rest of parameters (database Id, templates) can be passed implicitly var greeting = await logic.Apply(configuration); Console.WriteLine(greeting) } } //Congratulations, John Smith! You won 110$! //Congratulations, Mary Louie! You won 30$! //Congratulations, Louis Slaughter! You won 47$! //¡Felicidades, John! Ganaste 110 $ //¡Felicidades, Mary! Ganaste 30 $ //¡Felicidades, Louis! Ganaste 47 $ } private static async Reader<string> GetGreeting(int userId) { var template = await Reader<string>.Read<Configuration>(cfg => cfg.GreetingTemplate); var fullName = await GetFullName(userId); var win = await GetWin(userId); return string.Format(template, fullName, win); } private static async Reader<string> GetFullName(int userId) { var template = await Reader<string>.Read<Configuration>(cfg => cfg.NameFormat); var firstName = await GetFirstName(userId); var lastName = await GetLastName(userId); return string.Format(template, firstName, lastName); } private static async Reader<string> GetFirstName(int userId) { var dataBase = await GetDataBase(); return await dataBase.GetFirstName(userId); } private static async Reader<string> GetLastName(int userId) { var dataBase = await GetDataBase(); return await dataBase.GetLastName(userId); } private static async Reader<int> GetWin(int userId) { var dataBase = await GetDataBase(); return await dataBase.GetWin(userId); } private static async Reader<Database> GetDataBase() { var dataBaseId = await Reader<int>.Read<Configuration>(cfg => cfg.DataBaseId); return Database.ConnectTo(dataBaseId); } } public class Database { public static Database ConnectTo(int id) { if (id == 100) { return new Database(); } throw new Exception("Wrong database"); } private Database() { } private static readonly (int Id, string FirstName, string LastName, int Win)[] Data = { (1, "John","Smith", 110), (2, "Mary","Louie", 30), (3, "Louis","Slaughter", 47), }; public async Task<string> GetFirstName(int id) { await Task.Delay(50); return Data.Single(i => i.Id == id).FirstName; } public async Task<string> GetLastName(int id) { await Task.Delay(50); return Data.Single(i => i.Id == id).LastName; } public async Task<int> GetWin(int id) { await Task.Delay(50); return Data.Single(i => i.Id == id).Win; } } 

Das Programm zeigt Grüße für einige Benutzer an, aber wir kennen ihre Namen nicht im Voraus, da wir nur ihre IDs haben. Daher müssen wir diese Informationen aus einer "Datenbank" lesen. Um eine Verbindung zur Datenbank herzustellen, müssen wir eine Verbindungskennung kennen und um eine Begrüßung zu erstellen, benötigen wir deren Vorlage und ... alle Informationen werden implizit über den asynchronen Reader übergeben .


Abhängigkeitsinjektion mit Async "Reader"


Im Vergleich zur klassischen Implementierung weist der Async-Reader einen Fehler auf - wir können keinen Typ eines übergebenen Kontexts angeben. Diese Einschränkung ergibt sich aus der Tatsache, dass der C # -Compiler nur einen einzigen generischen Typparameter in einer Builder-Klasse für asynchrone Methoden erwartet (möglicherweise wird er in Zukunft behoben).


Andererseits halte ich es nicht für kritisch, da in einem realen Leben höchstwahrscheinlich ein Abhängigkeitsinjektionscontainer als Kontext übergeben wird:


 public static class Reader { public static Reader<TService> GetService<TService>() => Reader<TService>.Read<IServiceProvider>(serviceProvider => (TService)serviceProvider .GetService(typeof(TService))); } 

 ... private static async Reader<string> Greet(string userName) { var service = await Reader.GetService<IGreater>(); return service.GreetUser(userName); } ... 

( hier finden Sie ein vollständiges Beispiel ... )


Im Gegensatz zum asynchronen "Vielleicht" , das ich in keinem Produktionscode empfohlen habe (wegen des Problems mit finally- Blöcken), würde ich in Betracht ziehen, den asynchronen Reader in realen Projekten als Ersatz (oder "zusätzlich zu") des zu verwenden traditioneller Ansatz der Abhängigkeitsinjektion (wenn alle Abhängigkeiten an einen Klassenkonstruktor übergeben werden), da der Reader einige Vorteile hat:


  1. Klasseneigenschaften, in denen Links zu injizierten Ressourcen gespeichert sind, sind nicht erforderlich. Tatsächlich besteht in Klassen überhaupt keine Notwendigkeit - die gesamte Logik kann in statischen Methoden implementiert werden.
  2. Die Verwendung des Readers fördert das Erstellen von nicht blockierendem Code, da alle Methoden asynchron sind und nichts Entwickler daran hindert, nicht blockierende Versionen von Eingabe- / Ausgabeoperationen zu verwenden.
  3. Code ist etwas "lesbarer" - jedes Mal, wenn wir den Reader als Ergebnistyp einer Methode sehen, wissen wir, dass er Zugriff auf einen impliziten Kontext erfordert.
  4. Der asynchrone Reader verwendet keine Reflexion.

Natürlich mag es einige Argumente gegen die Verwendung des Readers geben, aber der Hauptzweck dieser Artikel besteht darin, zu zeigen, wie die Muster, die ursprünglich für funktionale Sprachen entworfen wurden, auf einen imperativen Codierungsstil angewendet werden können, der als einfacher angesehen wird von den meisten Menschen verstanden.

Source: https://habr.com/ru/post/de461371/


All Articles