«Reader» monad through async-await in C#

в 12:50, , рубрики: .net, C#, c#. functional style, monads, функциональное программирование

«Reader» monad through async-await in C# - 1
In my previous article I described how to achieve the "Maybe" monad behavior using async/await operators. This time I am going to show how to implement another popular design pattern "Reader Monad" using the same techniques.

That pattern allows implicit passing some context into some function without using function parameters or shared global objects and it can be considered as yet another way to implement dependency injection. For example:

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

Classic "Reader"

First, let's take a look at how the monad can be implemented without async/await operators:

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

(Reader)

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

The code works but it does not look natural for C# developers. No wonder, because monads came from functional languages where a similar code can be written in a more concise way. However, the classic implementation helps understating the essence of the pattern — instead of immediate execution of some code it is put into a function which will be called when its context is ready.


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 can combine several such functions into a single one, so you can create a sub-routine whose execution will be deferred until its context is applied. On the other hand, that approach resembles writing asynchronous code where program execution is stopped if some async operation is running. When a result of the operation is ready the program execution will continued. An assumption arises that the C# infrastructure designed to work with asynchronous operations (async/await) could be somehow utilized in implementation of "Reader" monad and… the assumption is correct!

Async "Reader"

In my previous article I demonstrated how to get control over async/await operators using Generalized async return types. The same technique will be used this time. Let's start with "Reader" class which will be used as a result type of asynchronous operations:

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

The class has two different responsibilities (theoretically we could create 2 classes):

  1. Extracting some value form a context when the context is applied
  2. Creation of a linked list of Reader instances which will be used to distribute a context over a call hierarchy.

For each responsibility we will use a separate constructor:

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() { }

When an instance of the Reader class is used as an argument of await operator the instance will receive a link to a continuation delegate which should be called only when an execution context is resolved and we can extract (from the context) some data which will be used in the continuation.
«Reader» monad through async-await in C# - 2

To create connections between parent and child "readers" let's create the method:

private IReader _child;

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

which will be called inside 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; }
}

Inside SetChild method we call SetCtx which propagates a context down to a hierarchy and calls an extractor if it is defined:

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 stores a value extracted from a context and calls a continuation:

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

In the case when a Reader instance does not have an initialized "extractor" then SetResult is supposed to be called by ReaderTaskMethodBuilder when a generated state machine goes to its final state.

Apply method just calls SetCtx

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

You can find all the code on github (if it is still not blocked)

Now, I want to show a more realistic example of how the async Reader can be used:

Click to expand the example

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

The program shows greetings for some users but we do not know their names in advance since we have just their ids, so we need to read that information from a "database". To connect to the database we need to know some connection identifier and to create a greeting we need its template and… all the information is passed implicitly trough the async Reader.

Dependency Injection with Async "Reader"

In comparison with the classic implementation the async reader has a flaw — we cannot specify a type of a passed context. This limitation comes from the fact that C# compiler expects just a single generic type parameter in an async method builder class (maybe it will be fixed in future).

On the other hand, I do not think it is critical since in a real life most probably some dependency injection container will be passed as a context:

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

(here you can find a full example...)

Unlike the async "Maybe", which I did not recommend using in any production code (because of the issue with finally blocks), I would consider using the async Reader in real projects as a replacement of (or "in addition to") the traditional dependency injection approach (when all dependencies are passed into a class constructor) since the Reader has some advantages:

  1. There is no need in class properties that store links to injected resources. Actually there is no need in classes at all — all logic can be be implemented in static methods.
  2. Using the Reader will encourage creating non blocking code since all methods will be asynchronous and nothing will prevent developers using non blocking versions of input/output operations.
  3. Code will be a little bit more "readable" — each time we see the Reader as a result type of some method we will know that it requires access to some implicit context.
  4. The async Reader does not use reflection.

Of course there might be some arguments against using the Reader but anyway, the main purpose of these articles is to show how the patterns, which were initially designed for functional languages, can be adopted to imperative style of coding which is believed to be simpler to understand by most people.

Автор: 0x1000000

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js