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.