Monad "Reader" a través de asíncrono / espera en C #


En mi artículo anterior, describí cómo implementar el patrón Quizás Monad usando declaraciones asíncronas / en espera . Esta vez le diré cómo implementar otra plantilla de diseño popular, "Monad Reader", utilizando las mismas técnicas.


Esta plantilla le permite transferir implícitamente un cierto contexto a la jerarquía de llamadas a funciones sin usar parámetros o campos de clase, y puede considerarse como otra forma de implementar la Inyección de dependencias. Por ejemplo:


class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"})); //(Hi, John!, Hi, José!) Console.WriteLine(await GreetGuys().Apply(new Config {Template = "¡Hola, {0}!" })); //(¡Hola, John!, ¡Hola, José!) } //       -   “Config". public static async Reader<(string gJohn, string gJose)> GreetGuys() => (await Greet("John"), await Greet("José")); 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); 

"Lector" clásico


Primero, veamos cómo puede implementar este patrón sin sentencias async / wait :


 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); //  "query syntax"      : //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)); } 

(Lector)


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

Este código funciona, pero sin el uso de la sintaxis de consulta (que no siempre es conveniente en sí misma), su legibilidad disminuye drásticamente y esto no es sorprendente, ya que las mónadas provienen de lenguajes funcionales donde dicho código parece natural y se lee bien (aunque incluso en Haskelel surgió un "do" notación para mejorar la legibilidad). Sin embargo, la implementación clásica ayuda a comprender la esencia del patrón: en lugar de ejecutar inmediatamente algún código, se coloca en una función que solo se llamará cuando obtenga su contexto.


 public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name)); //      ,     : //public static string Greet(string name, Config cfg) // => string.Format(cfg.Template, name); 

SelectMany puede agrupar varias de estas funciones en una sola, por lo que puede crear una rutina completa que se retrasará hasta que se aplique su contexto. Por otro lado, este enfoque se asemeja a escribir código asincrónico, donde la ejecución del programa se detiene si se requiere el resultado de alguna operación asincrónica. Cuando el resultado de la operación esté listo, el programa continuará. Se supone que la infraestructura de C #, diseñada para trabajar con operaciones asíncronas ( asíncrono / espera ), se puede utilizar de alguna manera al implementar la mónada "Reader" y ... ¡esta suposición es cierta! Si las funciones requieren acceso al contexto, entonces su ejecución puede "pausarse" hasta que este contexto se especifique externamente.


Lector asincrónico


En mi artículo anterior, mostré cómo obtener control sobre las declaraciones asíncronas / en espera usando tipos de retorno asincrónicos genéricos . El mismo enfoque se utilizará esta vez. Comencemos con la clase Reader , que se usará como el tipo de resultado de operaciones asincrónicas:


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

Esta clase tiene dos tareas (en teoría, podríamos crear dos clases diferentes):


  1. Recuperando valores del contexto.
  2. Cree una lista vinculada de instancias de la clase Reader que se utilizará para distribuir el contexto en toda la jerarquía de llamadas.

Para cada una de estas tareas, crearemos un constructor separado:


 private readonly Func<object, T> _extractor; //1.       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.   ReaderTaskMethodBuilder     C# internal Reader() { } 

Cuando se usa una instancia de la clase Reader como argumento para el operador de espera , esta instancia obtiene una referencia al delegado que continuará el programa. Se debe llamar a este delegado después de recibir el contexto de la extracción de los datos necesarios para que el programa continúe.



Para vincular instancias de la clase Reader , creemos el método SetChild :


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

que se llamará dentro de ReaderTaskMethodBuilder :


 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; } } 

Dentro del método SetChild, llamamos a la función SetCtx para difundir el contexto a través de la jerarquía de llamadas. Si, al llamar a SetCtx en este nivel de la jerarquía, se especifica la función _ extractor (el primer constructor de la clase Reader ) que extrae directamente los datos del contexto, ahora puede llamarlos, obtener los datos necesarios y completar la operación asincrónica actual a través de una llamada a SetResult :


 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 almacena el valor recuperado del contexto y llama al delegado para continuar el programa:


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

En el caso de que la instancia de la clase Reader no tenga iniciada la función _ extractor (el segundo constructor de la clase Reader ), ReaderTaskMethodBuilder debe llamar a SetResult cuando la máquina de estado generada pasa al estado final.


SetCtx también se llama en el método Apply para establecer el contexto en el nodo raíz de la jerarquía:


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

Código completo de GitHub


Ahora, puede echar un vistazo a un ejemplo más realista del uso de Reader asíncrono -a:


Haga clic para expandir el ejemplo.
 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) { //""      - userId var logic = GetGreeting(userId); //  (database Id, templates)     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; } } 

El programa muestra saludos para algunos usuarios, pero no sabemos sus nombres de antemano porque solo tenemos sus identificadores, por lo que debemos leer sus nombres de la "base de datos". Para conectarse a esta "base de datos" necesita conocer los parámetros de conexión, y para generar saludos, también necesitamos plantillas para estos saludos. Toda esta información se pasa implícitamente a través del lector asíncrono.


Inyección de dependencia a través de "Reader" asíncrono


En comparación con la implementación clásica, el lector asíncrono tiene un inconveniente: no podemos especificar el tipo de contexto que se transmitirá. Esta limitación proviene del hecho de que el compilador de C # solo permite un tipo de datos parametrizado (tipo genérico) en la clase ReaderTaskMethodBuilder (esto puede corregirse en futuras versiones).


Por otro lado, no creo que esto sea crítico, porque en la vida real un contenedor de inyección de dependencia probablemente se pasará como contexto:


 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); } ... 

( Aquí puedes encontrar la versión completa ... )


A diferencia del asincrónico "Quizás" , que no recomendé usar en ningún código industrial, consideraría usar el Asíncrono Reader -a en algunos proyectos de la vida real como reemplazo (o adición) a los mecanismos tradicionales de inyección de dependencias (cuando todas las dependencias se pasan como parámetros del constructor) Dado que Reader -a tiene una serie de ventajas:


  1. No hay necesidad de campos cuyas clases almacenen enlaces a recursos integrados. De hecho, no habrá necesidad de clases reales, ya que toda la lógica se puede implementar en métodos estáticos.
  2. El uso de Reader -a tenderá a escribir código sin bloqueo, ya que todos los métodos serán asincrónicos y nada interferirá con el uso de versiones asincrónicas de las funciones de la biblioteca.
  3. El código será un poco más legible, porque cada vez que veamos a Reader como el tipo de retorno de algún método, sabremos que requiere acceso a algún contexto implícito
  4. El lector asíncrono no usa la reflexión.

Por supuesto, puede haber objeciones al uso de este Reader -a, pero en cualquier caso, la tarea principal de estos artículos es mostrar cómo las plantillas que se crearon originalmente para lenguajes funcionales pueden adaptarse para un estilo de programación imperativo, que la mayoría de las personas considera más fácil de entender. .

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


All Articles