我有一个运行良好的示例铁路管道:
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
最佳答案
Same output/input... What's wrong?
不,它们没有相同的输出/输入。
如果您查看
(>>=)
的类型类似于 'Monad<'T> -> ('T -> 'Monad<'U>) -> 'Monad<'U>
这是泛型绑定(bind)操作的伪造签名,通常为 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>
.如果您考虑一下,这是有道理的。这就像应用替换 Monad<_>
与 Result<_,'TError>
你的论点被翻转了,这就是我们使用 |>
的原因.那么你的函数都是
int -> Result<_,'TError>
所以类型匹配,这是有道理的(并且有效)。现在,转到您的第二个代码片段,函数
funB
有不同的签名,它有 Async<Result<_,'TError>>
所以现在类型不匹配。而且也有道理,不能使用Result
的绑定(bind)实现对于 Async
.那么,解决方案是什么?
最简单的解决方案是不要使用绑定(bind),至少不要使用 2 个单子(monad)。你可以将你的第一个函数“提升”到
Async
并使用 async.Bind
, 使用通用 >>=
或标准异步工作流程,但在其中您必须使用手册 match
将结果绑定(bind)到第二个函数。另一种方法更有趣,但也更难理解,它包括使用称为 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)变压器, 所以它有一个绑定(bind)操作来处理外部 monad 的绑定(bind),在我们的例子中是 Async
.然后我们简单地包装
funB
进入 ResultT
在函数的最后,我们从 ResultT
展开与 Result.run
.有关 F# 中分层 monad 的更多示例,see these questions
还有其他方法,一些库提供了一些“神奇的工作流程”,它使用临时重载将 monad 与组合 monad(也称为分层 monad)结合起来,因此您编写的代码更少,但推理类型并不容易,因为重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。
注意:这样的编码是一个很好的练习,但在现实生活中也可以考虑使用异常,以免代码过于复杂。
关于f# - 使用 bind 编写跨世界的异步函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57190595/