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.
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With