使用 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)结合起来,因此您编写的代码更少,但并不那么容易推理关于类型,由于重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。
注意:像这样编码是一个很好的练习,但在现实生活中也要考虑使用异常,以免代码过于复杂。
我有一个运行良好的示例铁路管道:
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)结合起来,因此您编写的代码更少,但并不那么容易推理关于类型,由于重载不遵循任何替换规则,您必须查看源代码以了解发生了什么。
注意:像这样编码是一个很好的练习,但在现实生活中也要考虑使用异常,以免代码过于复杂。