如何对异步函数进行monadic绑定?

时间:2018-01-17 20:04:53

标签: c# async-await bind monads

考虑一下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;
}

它的目的是处理一系列以干净的方式返回可选值的函数:

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

在上述情况下,GetOrderGetClient都会返回Maybe<T>,但None案例的处理隐藏在Bind内。到目前为止一切都很好。

但是如何将Maybe<T>绑定到async函数,即返回Task<Maybe<T>>的函数?例如,以下代码因编译器错误而失败,因为Bind需要Func<T, Maybe<U>>而不是Func<T, Task<Maybe<U>>>

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

我尝试await传递给Task的lambda中的Bind,但这迫使我添加Bind的重载,接受返回{{1}的函数}}:

Task

如您所见,代码不再运行public Maybe<U> Bind<U>(Func<T, Task<Maybe<U>>> f) => HasValue ? f(value).Result : Maybe<U>.None; ,而是使用async阻止。 MEH。

第二次尝试是Resultawait内的任务:

Bind

但现在public async Task<Maybe<U>> Bind<U>(Func<T, Task<Maybe<U>>> f) => HasValue ? await f(value) : Maybe<U>.None; 必须将Bind包裹在Maybe<T>中并且链接看起来很难看:

Task

对此有更好的解决方案吗?

我创建了一个fully working example,以防我在解释中遗漏了一些细节。

1 个答案:

答案 0 :(得分:4)

我想我找到了一个很好的解决方案。我们的想法是将Task<Maybe<T>>扩展为两个Bind函数,这些函数基本上将等待转发到Maybe<T>链中的第一个函数:

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

有了这些,我们可以将函数绑定到Maybe<T>任务,直接返回Maybe<T>或另一个Maybe<T>任务:

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

工作示例:https://dotnetfiddle.net/Kekp0S