9

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.

Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65

1 Answers1

10

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

Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65