我该如何对异步函数进行单子绑定?

9
考虑一下这个基本的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;
}

它的目的是以简洁的方式处理返回可选值的函数链:

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

我试图在传递给Bind的lambda内部使用await Task,但这迫使我添加了一个重载函数来接受返回Task的函数:

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

如您所见,代码不再使用async,而是使用Result进行阻塞。不太好。

第二次尝试是在新的Bindawait任务:

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

但是现在Bind必须将Maybe<T>包装在Task中,链式调用看起来很丑陋:

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

有没有更好的解决办法?

我创建了一个完全可用的示例,以防我在解释中漏掉了一些细节。

1个回答

10

我认为我找到了一个好的解决方案。这个想法是通过两个Bind函数扩展Task<Maybe<T>>,它们基本上将等待转发到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));

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接