使用绑定编写跨越世界的异步函数



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

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转换为异步函数时,我遇到了编译错误。从逻辑上讲,它不应该有所不同。相同的输出/输入...怎么了?

应该怎么做才能让它工作?!

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

相同的输出/输入...怎么了?

不,它们没有相同的输出/输入。

如果您查看(>>=)的类型,它会像'Monad<'T> -> ('T -> 'Monad<'U>) -> 'Monad<'U>,这是通用绑定操作的假签名,通常对于Monads来说是重载的。在你的第一个例子中,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

那么,解决方案是什么?

最简单的解决方案是不要使用 bind,至少不要使用 2 个 monads。你可以"提升"你的第一个函数来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

因此,我们在这里所做的是我们将funA函数"提升"到Async,然后将其包装在ResultT中,这是一个用于Result的monad变压器,因此它具有绑定操作,该操作也负责在外部monad上进行绑定,在我们的例子中Async

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

有关 F# 中分层单子的更多示例,请参阅这些问题

还有其他方法,一些库提供了一些"魔术工作流程",它使用临时重载将 monad 与组合 monad(又名分层 monads)组合在一起,因此您编写的代码更少,但推理类型并不容易,因为重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。

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

相关内容

  • 没有找到相关文章

最新更新