我有一个运行良好的铁路管道示例:
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.run
从ResultT
中解包。
有关 F# 中分层单子的更多示例,请参阅这些问题
还有其他方法,一些库提供了一些"魔术工作流程",它使用临时重载将 monad 与组合 monad(又名分层 monads)组合在一起,因此您编写的代码更少,但推理类型并不容易,因为重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。
注意:像这样编码是一个很好的练习,但在现实生活中也考虑使用异常,以免使代码过于复杂。