akh*_*ari 4 f# monad-transformers f#+
我有一个运行良好的示例铁路管道:
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
Run Code Online (Sandbox Code Playgroud)
但是,当我想将其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
Run Code Online (Sandbox Code Playgroud)
相同的输出/输入...怎么了?
不,他们没有相同的输出/输入。
如果您看一下(>>=)它的类型,那'Monad<'T> -> ('T -> 'Monad<'U>) -> 'Monad<'U>是通用绑定操作的虚假签名,通常对Monads来说是重载的。在第一个示例中,Monad为Result<_,'TError>,因此您的第一个示例可以重写为:
let funC n = // int -> Result<(int * int), string>
n
|> funA
|> Result.bind funB
Run Code Online (Sandbox Code Playgroud)
的签名Result.bind为('T -> Result<'U,'TError>) -> Result<'T,'TError> -> Result<'U,'TError>。如果您考虑一下,这是有道理的。就像使用替换Monad<_>,Result<_,'TError>并且您将参数翻转了一样,这就是我们使用的原因|>。
然后,您的函数都是相同的,int -> Result<_,'TError>因此类型匹配,并且很有意义(并且可以工作)。
现在,转到第二个代码片段,该函数funB具有不同的签名,Async<Result<_,'TError>>因此现在类型不匹配。而且这也很有意义,您不能将的绑定实现Result用于Async。
那么,有什么解决方案?
最简单的解决方案是不要使用绑定,至少不要使用2个monad。您可以将第一个功能“提升”到通用或标准异步工作流程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
Run Code Online (Sandbox Code Playgroud)
因此,我们在这里所做的是将funA函数“提升” 到Async,然后将其包装ResultT为monad转换器Result,因此在我们的情况下,它具有绑定操作,该操作也要在外部monad上进行绑定Async。
然后,我们简单地包装funB到ResultT函数中,并在函数的末尾ResultT使用Result.run。
有关F#中分层Monad的更多示例,请参阅以下问题
还有其他方法,有些库提供了一些“魔术工作流”,它们使用临时重载将单子与组合单子(又称为分层单子)组合在一起,因此您编写的代码更少,但是由于类型的原因并不那么容易,因为重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。
注意:这样的编码是一个很好的练习,但是在现实生活中,请考虑使用异常,以免使代码过于复杂。