Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I do a monadic bind to an async function?

Consider this basic implementation of the maybe monad:

public class Maybe<T>
{
    private readonly T value;

    private Maybe(bool hasValue, T value) : this(hasValue) => this.value = value;

    private Maybe(bool hasValue) => HasValue = hasValue;

    public bool HasValue {get;}

    public T Value => HasValue ? value : throw new InvalidOperationException();

    public static Maybe<T> None {get;} = new Maybe<T>(false);

    public static Maybe<T> Some(T value) => new Maybe<T>(true, value);

    public Maybe<U> Bind<U>(Func<T, Maybe<U>> f) => HasValue ? f(value) : Maybe<U>.None;
}

Its purpose is to handle a chain of functions returning optional values in a clean way:

var client = Maybe<int>.Some(1)
    .Bind(orderId => GetOrder(orderId))
    .Bind(order => GetClient(order.ClientId));
Console.WriteLine(client);

In the above case both GetOrder and GetClient return a Maybe<T>, but handling of the None case is hidden inside Bind. So far so good.

But how would I bind a Maybe<T> to an async function, i.e. a function returning Task<Maybe<T>> instead? For instance the following code fails with compiler errors, because Bind expects a Func<T, Maybe<U>> rather than a Func<T, Task<Maybe<U>>>:

var client = Maybe<int>.Some(1)
    .Bind(orderId => GetOrderAsync(orderId))
    .Bind(order => GetClientAsync(order.ClientId));
Console.WriteLine(client);

I tried to await the Task inside the lambda passed to Bind, but that forced me to add an overload of Bind that accepts functions returning a Task:

public Maybe<U> Bind<U>(Func<T, Task<Maybe<U>>> f) 
    => HasValue ? f(value).Result : Maybe<U>.None;

As you can see, the code is not running async anymore, and instead blocks with Result. Meh.

Second try was to await the task inside the new Bind:

public async Task<Maybe<U>> Bind<U>(Func<T, Task<Maybe<U>>> f) 
    => HasValue ? await f(value) : Maybe<U>.None;

But now Bind has to wrap the Maybe<T> in a Task and chaining will look ugly:

var asyncClient = await (await Maybe<int>.Some(2)
    .Bind(orderId => GetOrderAsync(orderId)))
    .Bind(order => GetClientAsync(order.ClientId));

Is there a nicer solution to this?

I created a fully working example, in case I missed some details in the explanation.

like image 692
Good Night Nerd Pride Avatar asked Jan 17 '18 20:01

Good Night Nerd Pride


People also ask

What happens when you call an async method?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

Is Linq a Monad?

Monad pattern of LINQ So LINQ SelectMany query's quintessential mathematics is monad. Generally, in DotNet category, a type is a monad if: This type is an open generic type definition, which can be viewed as type constructor of kind * –> *, so that it maps a concrete type to another concrete monad-wrapped type.

Is task a Monad?

NET Tasks aren't proper monads, but you mostly observe the difference when you perform impure operations. As a general observation, when impure operations are allowed, the conclusions of this overall article series are precarious.


1 Answers

I think I found a good solution. The idea is to extend Task<Maybe<T>> with two Bind functions that basically forward the await to the first function in the Maybe<T> chain:

public static class TaskExtensions 
{
    public static async Task<Maybe<U>> Bind<T, U>(
        this Task<Maybe<T>> task, Func<T, Maybe<U>> f) 
        => (await task).Bind(f);

    public static async Task<Maybe<U>> Bind<T, U>(
        this Task<Maybe<T>> task, Func<T, Task<Maybe<U>>> f) 
        => await (await task).Bind(f);
}

Armed with these we can bind functions to Maybe<T> tasks that either return a Maybe<T> directly or another Maybe<T> task:

// Notice how we only have to await once at the top.
var asyncClient = await Maybe<int>.Some(2)
    .Bind(orderId => GetOrderAsync(orderId))
    .Bind(order => GetClientAsync(order.ClientId));

Working example: https://dotnetfiddle.net/Kekp0S

like image 53
Good Night Nerd Pride Avatar answered Oct 19 '22 01:10

Good Night Nerd Pride