Mônada "Reader" via assíncrono / aguardar em C #


No meu artigo anterior, descrevi como implementar o padrão Maybe Monad usando instruções async / wait . Desta vez, mostrarei como implementar outro padrão de design popular, o "Monad Reader", usando os mesmos truques.


Este modelo permite transferir implicitamente um determinado contexto para a hierarquia de chamadas de função sem usar parâmetros ou campos de classe, e pode ser considerado como outra maneira de implementar a Injeção de Dependência. Por exemplo:


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

"Leitor" clássico


Primeiro, vamos ver como você pode implementar esse padrão sem instruções async / waitit :


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

(Leitor)


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

Esse código funciona, mas sem o uso da sintaxe de consulta (que nem sempre é conveniente), sua legibilidade diminui drasticamente e isso não é surpreendente, uma vez que as mônadas vieram de linguagens funcionais em que esse código parece natural e lê bem (embora mesmo em Haskelel tenha feito um "do" notação para melhorar a legibilidade). No entanto, a implementação clássica ajuda a entender a essência do padrão - em vez de executar imediatamente algum código, ele é colocado em uma função que só será chamada quando receber seu 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); 

O SelectMany pode agrupar várias dessas funções em uma, para que você possa criar uma rotina inteira que será adiada até que seu contexto seja aplicado. Por outro lado, essa abordagem se assemelha à escrita de código assíncrono, onde a execução do programa é interrompida se o resultado de alguma operação assíncrona for necessário. Quando o resultado da operação estiver pronto, o programa continuará. Há uma suposição de que a infraestrutura C #, projetada para trabalhar com operações assíncronas ( assíncrona / aguardar ), possa ser usada de alguma maneira ao implementar a mônada "Reader" e ... essa suposição é verdadeira! Se as funções exigirem acesso ao contexto, sua execução poderá ser "pausada" até que esse contexto seja especificado externamente.


Leitor assíncrono


No meu artigo anterior, mostrei como obter controle sobre instruções assíncronas / esperadas usando tipos de retorno assíncrono genérico . A mesma abordagem será usada desta vez. Vamos começar com a classe Reader , que será usada como o tipo de resultado das operações assíncronas:


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

Esta classe tem duas tarefas (teoricamente, poderíamos criar duas classes diferentes):


  1. Recuperando valores do contexto.
  2. Crie uma lista vinculada de instâncias da classe Reader que serão usadas para distribuir o contexto por toda a hierarquia de chamadas.

Para cada uma dessas tarefas, criaremos um construtor 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() { } 

Quando uma instância da classe Reader é usada como argumento para o operador wait , essa instância obtém uma referência ao delegado que continuará o programa. Esse delegado deve ser chamado após receber o contexto da extração dos dados necessários para a continuação do programa.



Para vincular instâncias da classe Reader , vamos criar o método SetChild :


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

que será chamado dentro do 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 do método SetChild, chamamos a função SetCtx para espalhar o contexto pela hierarquia de chamadas. Se, ao chamar SetCtx neste nível da hierarquia, a função _ extractor (o primeiro construtor da classe Reader ) foi especificada para extrair diretamente dados do contexto, agora você pode chamá-lo, obter os dados necessários e concluir a operação assíncrona atual por meio de uma chamada para 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 armazena o valor recuperado do contexto e chama o delegado para continuar o programa:


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

No caso em que a instância da classe Reader não tenha a função extrator _ iniciada (o segundo construtor da classe Reader ), SetResult deverá ser chamado por ReaderTaskMethodBuilder quando a máquina de estado gerada for para o estado final.


SetCtx também é chamado no método Apply para definir o contexto no nó raiz da hierarquia:


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

Código GitHub completo


Agora, você pode dar uma olhada em um exemplo mais realista do uso do Reader assíncrono -a:


Clique para expandir o exemplo.
 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; } } 

O programa mostra cumprimentos para alguns usuários, mas não sabemos seus nomes com antecedência porque só temos seus identificadores, portanto, precisamos ler seus nomes no "banco de dados". Para conectar-se a esse "banco de dados", você precisa conhecer os parâmetros de conexão e gerar saudações, também precisamos de modelos para essas saudações. Toda essa informação é passada implicitamente pelo leitor assíncrono.


Injeção de dependência via "Reader" assíncrono


Comparado à implementação clássica, o Reader assíncrono tem uma desvantagem - não podemos especificar o tipo de contexto a ser transmitido. Essa limitação vem do fato de que o compilador C # permite apenas um tipo de dados parametrizado (tipo genérico) na classe ReaderTaskMethodBuilder (isso pode ser corrigido em versões futuras).


Por outro lado, não acho que isso seja crítico, porque na vida real um recipiente de injeção de dependência provavelmente será passado como um 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); } ... 

( Aqui você pode encontrar a versão completa ... )


Diferentemente do "Maybe" assíncrono , que eu não recomendo usar em nenhum código industrial, consideraria usar o Reader assíncrono -a em alguns projetos da vida real como uma substituição (ou adição) aos mecanismos tradicionais de injeção de dependência (quando todas as dependências são passadas como parâmetros de construtor) já que o Reader -a tem várias vantagens:


  1. Não há necessidade de campos cujas classes armazenam links para recursos incorporados. De fato, não haverá necessidade de classes reais, pois toda lógica pode ser implementada em métodos estáticos.
  2. O uso do Reader -a tenderá a escrever código sem bloqueio, pois todos os métodos serão assíncronos e nada interferirá no uso de versões assíncronas das funções da biblioteca.
  3. O código ficará um pouco mais legível, porque toda vez que virmos o Reader como o tipo de retorno de algum método, saberemos que ele requer acesso a algum contexto implícito
  4. O leitor assíncrono não usa reflexão.

Obviamente, pode haver objeções ao uso deste Reader -a, mas, em qualquer caso, a principal tarefa desses artigos é mostrar como os modelos criados originalmente para linguagens funcionais podem ser adaptados para um estilo de programação imperativo, considerado pela maioria das pessoas como mais fácil de entender .

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


All Articles