使用 bind 编写跨世界的异步函数

Compose world-crossing async functions with bind

我有一个运行良好的示例铁路管道:

open FSharpPlus

let funA n =
    if n < 10 then Ok n
    else Error "not less than 10"

let funB n =
    if n < 5 then Ok (n, n * 2)
    else Error "not less than 5"

let funC n = // int -> Result<(int * int), string>
    n
    |> funA
    >>= funB // it works

但是当我想将 funB 转换为异步函数时,我遇到了编译错误。从逻辑上讲,它应该没有什么不同。一样output/input...怎么了?

应该怎么做才能让它发挥作用?!

open FSharpPlus

let funA n =
    if n < 10 then Ok n
    else Error "not less than 10"

let funB n = async {
    if n < 5 then return Ok (n, n * 2)
    else return Error "not less than 5" }

let funC n = // int -> Async<Result<(int * int), string>>
    n
    |> funA
    >>= funB // compile error

Same output/input... What's wrong?

不,他们没有相同的output/input。

如果您查看 (>>=) 的类型,它类似于 'Monad<'T> -> ('T -> 'Monad<'U>) -> 'Monad<'U>,它是通用绑定操作的伪造签名,通常为 Monad 重载。在您的第一个示例中,Monad 是 Result<_,'TError>,因此您的第一个示例可以重写为:

let funC n = // int -> Result<(int * int), string>
    n
    |> funA
    |> Result.bind funB

Result.bind的签名是('T -> Result<'U,'TError>) -> Result<'T,'TError> -> Result<'U,'TError>。如果您考虑一下,这是有道理的。这就像用 Result<_,'TError> 应用替换 Monad<_> 并且您翻转了参数,这就是我们使用 |>.

的原因

那么你的函数都是 int -> Result<_,'TError> 所以类型匹配,这很有意义(而且有效)。

现在,转到您的第二个代码片段,函数 funB 具有不同的签名,它具有 Async<Result<_,'TError>>,因此现在类型不匹配。这也是有道理的,您不能将 Result 的绑定实现用于 Async.

那么,解决方案是什么?

最简单的解决方案是不使用绑定,至少不对 2 个单子使用。您可以 "lift" 您的第一个函数到 Async 并使用 async.Bind,使用通用 >>= 或标准异步工作流程,但在其中您必须使用手动 match 将结果绑定到第二个函数。

另一种方法更有趣,但也更难理解,它包括使用称为 Monad Transformers 的抽象:

open FSharpPlus.Data

let funC n = // int -> Result<(int * int), string>
    n
    |> (funA >> async.Return >> ResultT)
    >>= (funB >> ResultT)
    |> ResultT.run

所以,我们在这里做的是 "lift" funA 函数到 Async,然后我们将它包装在 ResultT 中,这是 Result,所以它有一个绑定操作,负责绑定外部 monad,在我们的例子中 Async.

然后我们简单地将 funB 包装到 ResultT 中,在函数的最后我们使用 Result.run.

ResultT 中解包

有关 F# 中分层 monad 的更多示例,see these questions

还有其他方法,一些库提供了一些 "magic workflows",它使用临时重载将 monad 与组合 monad(也称为分层 monad)结合起来,因此您编写的代码更少,但并不那么容易推理关于类型,由于重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。

注意:像这样编码是一个很好的练习,但在现实生活中也要考虑使用异常,以免代码过于复杂。