具有异步操作的面向铁路的编程
Railway oriented programming with Async operations
之前问过类似的问题,但不知何故我找不到出路,再次尝试另一个例子。
可在 https://ideone.com/zkQcIU 获得作为起点的代码(略有修整)。
(它在识别 Microsoft.FSharp.Core.Result
类型时遇到一些问题,不知道为什么)
基本上所有操作都必须通过前一个函数将结果传递给下一个函数进行流水线处理。这些操作必须是异步的,如果发生异常,它们应该 return 向调用者发送错误。
要求是给调用者结果或错误。所有函数 return 填充有 Success type Article
或 Failure 的元组 type Error
对象具有描述性 code
和 message
return 来自服务器。
将感谢围绕我的代码的工作示例,同时为被调用者和调用者提供答案。
被叫代码
type Article = {
name: string
}
type Error = {
code: string
message: string
}
let create (article: Article) : Result<Article, Error> =
let request = WebRequest.Create("http://example.com") :?> HttpWebRequest
request.Method <- "GET"
try
use response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
Ok ((new DataContractJsonSerializer(typeof<Article>)).ReadObject(memoryStream) :?> Article)
with
| :? WebException as e ->
use reader = new StreamReader(e.Response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
Error ((new DataContractJsonSerializer(typeof<Error>)).ReadObject(memoryStream) :?> Error)
其余链接方法 - 相同的签名和相似的主体。实际上,您可以为 update
、upload
和 publish
重用 create
的主体,以便能够测试和编译代码。
let update (article: Article) : Result<Article, Error>
// body (same as create, method <- PUT)
let upload (article: Article) : Result<Article, Error>
// body (same as create, method <- PUT)
let publish (article: Article) : Result<Article, Error>
// body (same as create, method < POST)
来电号码
let chain = create >> Result.bind update >> Result.bind upload >> Result.bind publish
match chain(schemaObject) with
| Ok article -> Debug.WriteLine(article.name)
| Error error -> Debug.WriteLine(error.code + ":" + error.message)
编辑
根据答案并将其与 Scott 的实现相匹配 (https://i.stack.imgur.com/bIxpD.png),以帮助比较和更好地理解。
let bind2 (switchFunction : 'a -> Async<Result<'b, 'c>>) =
fun (asyncTwoTrackInput : Async<Result<'a, 'c>>) -> async {
let! twoTrackInput = asyncTwoTrackInput
match twoTrackInput with
| Ok s -> return! switchFunction s
| Error err -> return Error err
}
编辑 2 基于 F# 实现的 bind
let bind3 (binder : 'a -> Async<Result<'b, 'c>>) (asyncResult : Async<Result<'a, 'c>>) = async {
let! result = asyncResult
match result with
| Error e -> return Error e
| Ok x -> return! binder x
}
看看 Suave source code,特别是 WebPart.bind
函数。在 Suave 中,WebPart 是一个接受上下文("context" 是当前请求和到目前为止的响应)和 return 类型 Async<context option>
的结果的函数。将这些链接在一起的语义是,如果 async returns None
,则跳过下一步;如果它 returns Some value
,则以 value
作为输入调用下一步。这与 Result
类型的语义几乎相同,因此您几乎可以复制 Suave 代码并针对结果而不是选项调整它。例如,像这样:
module AsyncResult
let bind (f : 'a -> Async<Result<'b, 'c>>) (a : Async<Result<'a, 'c>>) : Async<Result<'b, 'c>> = async {
let! r = a
match r with
| Ok value ->
let next : Async<Result<'b, 'c>> = f value
return! next
| Error err -> return (Error err)
}
let compose (f : 'a -> Async<Result<'b, 'e>>) (g : 'b -> Async<Result<'c, 'e>>) : 'a -> Async<Result<'c, 'e>> =
fun x -> bind g (f x)
let (>>=) a f = bind f a
let (>=>) f g = compose f g
现在你可以这样写你的链了:
let chain = create >=> update >=> upload >=> publish
let result = chain(schemaObject) |> Async.RunSynchronously
match result with
| Ok article -> Debug.WriteLine(article.name)
| Error error -> Debug.WriteLine(error.code + ":" + error.message)
警告:我无法通过 运行 在 F# Interactive 中验证此代码,因为我没有您的 create/update/etc 的任何示例。职能。原则上它应该可以工作——所有类型都像乐高积木一样组合在一起,这就是你如何判断 F# 代码可能是正确的——但如果我犯了编译器会发现的拼写错误,我还没有了解它。让我知道这是否适合你。
更新: 在评论中,您询问是否需要同时定义 >>=
和 >=>
运算符,并提到您没有在 chain
代码中看不到它们的使用。我定义两者是因为它们有不同的用途,就像 |>
和 >>
运算符有不同的用途一样。 >>=
类似于 |>
:它将 值 传递给 函数 。虽然 >=>
类似于 >>
:它采用 两个函数 并将它们组合在一起。如果您要在 non-AsyncResult 上下文中编写以下内容:
let chain = step1 >> step2 >> step3
然后转换为:
let asyncResultChain = step1AR >=> step2AR >=> step3AR
我使用 "AR" 后缀来指示 return 和 Async<Result<whatever>>
类型的那些函数的版本。另一方面,如果您以 pass-the-data-through-the-pipeline 风格编写:
let result = input |> step1 |> step2 |> step3
那么这将转化为:
let asyncResult = input >>= step1AR >>= step2AR >>= step3AR
所以这就是为什么你需要 bind
和 compose
函数,以及对应于它们的运算符:这样你就可以得到 |>
或>>
个用于 AsyncResult 值的运算符。
顺便说一句,我选的运算符"names"(>>=
和>=>
),我不是随便选的。这些是标准运算符,用于对 Async、Result 或 AsyncResult 等值进行 "bind" 和 "compose" 操作。因此,如果您要定义自己的名称,请坚持使用 "standard" 运算符名称,这样阅读您的代码的其他人就不会感到困惑。
更新 2:阅读这些类型签名的方法如下:
'a -> Async<Result<'b, 'c>>
这是一个采用类型 A 的函数,并且 return 是一个 Async
环绕 Result
。 Result
成功案例为类型 B,失败案例为类型 C。
Async<Result<'a, 'c>>
这是一个值,不是函数。它是 Async
包裹着 Result
,其中类型 A 是成功案例,类型 C 是失败案例。
所以bind
函数有两个参数:
- 从 A 到异步(B 或 C)的函数。
- 一个异步值(A 或 C))。
它 returns:
- 一个异步值(B 或 C)。
查看这些类型签名,您已经可以开始了解 bind
函数的作用。它将采用 A 或 C 的值,并且 "unwrap" 它。如果它是 C,它将产生一个 "either B or C" 值,它是 C(并且不需要调用该函数)。如果是 A,那么为了将其转换为 "either B or C" 值,它将调用 f
函数(接受 A)。
所有这些都发生在异步上下文中,这给类型增加了一层额外的复杂性。如果您查看不涉及异步的 basic version of Result.bind
,可能更容易掌握所有这些内容:
let bind (f : 'a -> Result<'b, 'c>) (a : Result<'a, 'c>) =
match a with
| Ok val -> f val
| Error err -> Error err
在此代码段中,val
的类型为 'a
,err
的类型为 'c
。
最终更新:聊天会话中有一条评论我认为值得保留在答案中(因为人们几乎从不关注聊天链接)。 Developer11 问,
... if I were to ask you what Result.bind
in my example code maps to your approach, can we rewrite it as create >> AsyncResult.bind update
? It worked though. Just wondering i liked the short form and as you said they have a standard meaning? (in haskell community?)
我的回复是:
Yes. If the >=>
operator is properly written, then f >=> g
will always be equivalent to f >> bind g
. In fact, that's precisely the definition of the compose
function, though that might not be immediately obvious to you because compose
is written as fun x -> bind g (f x)
rather than as f >> bind g
. But those two ways of writing the compose function would be exactly equivalent. It would probably be very instructive for you to sit down with a piece of paper and draw out the function "shapes" (inputs & outputs) of both ways of writing compose.
为什么要在这里使用面向铁路的编程?如果您只想 运行 一系列操作和 return 有关发生的第一个异常的信息,那么 F# 已经为此使用异常提供了语言支持。为此,您不需要面向铁路的编程。只需将您的 Error
定义为例外:
exception Error of code:string * message:string
修改代码以抛出异常(另请注意,您的 create
函数使用 article
但未使用它,因此我将其删除):
let create () = async {
let ds = new DataContractJsonSerializer(typeof<Error>)
let request = WebRequest.Create("http://example.com") :?> HttpWebRequest
request.Method <- "GET"
try
use response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
return ds.ReadObject(memoryStream) :?> Article
with
| :? WebException as e ->
use reader = new StreamReader(e.Response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
return raise (Error (ds.ReadObject(memoryStream) :?> Error)) }
然后您可以使用 let!
在 async
块中对它们进行排序并添加异常处理来组合函数:
let main () = async {
try
let! created = create ()
let! updated = update created
let! uploaded = upload updated
Debug.WriteLine(uploaded.name)
with Error(code, message) ->
Debug.WriteLine(code + ":" + message) }
如果您想要更复杂的异常处理,那么面向铁路的编程可能会有用,并且肯定有一种方法可以将它与 async
集成,但是如果您只想做您在问题中描述的事情,那么您只需使用标准 F# 就可以更轻松地做到这一点。
之前问过类似的问题,但不知何故我找不到出路,再次尝试另一个例子。
可在 https://ideone.com/zkQcIU 获得作为起点的代码(略有修整)。
(它在识别 Microsoft.FSharp.Core.Result
类型时遇到一些问题,不知道为什么)
基本上所有操作都必须通过前一个函数将结果传递给下一个函数进行流水线处理。这些操作必须是异步的,如果发生异常,它们应该 return 向调用者发送错误。
要求是给调用者结果或错误。所有函数 return 填充有 Success type Article
或 Failure 的元组 type Error
对象具有描述性 code
和 message
return 来自服务器。
将感谢围绕我的代码的工作示例,同时为被调用者和调用者提供答案。
被叫代码
type Article = {
name: string
}
type Error = {
code: string
message: string
}
let create (article: Article) : Result<Article, Error> =
let request = WebRequest.Create("http://example.com") :?> HttpWebRequest
request.Method <- "GET"
try
use response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
Ok ((new DataContractJsonSerializer(typeof<Article>)).ReadObject(memoryStream) :?> Article)
with
| :? WebException as e ->
use reader = new StreamReader(e.Response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
Error ((new DataContractJsonSerializer(typeof<Error>)).ReadObject(memoryStream) :?> Error)
其余链接方法 - 相同的签名和相似的主体。实际上,您可以为 update
、upload
和 publish
重用 create
的主体,以便能够测试和编译代码。
let update (article: Article) : Result<Article, Error>
// body (same as create, method <- PUT)
let upload (article: Article) : Result<Article, Error>
// body (same as create, method <- PUT)
let publish (article: Article) : Result<Article, Error>
// body (same as create, method < POST)
来电号码
let chain = create >> Result.bind update >> Result.bind upload >> Result.bind publish
match chain(schemaObject) with
| Ok article -> Debug.WriteLine(article.name)
| Error error -> Debug.WriteLine(error.code + ":" + error.message)
编辑
根据答案并将其与 Scott 的实现相匹配 (https://i.stack.imgur.com/bIxpD.png),以帮助比较和更好地理解。
let bind2 (switchFunction : 'a -> Async<Result<'b, 'c>>) =
fun (asyncTwoTrackInput : Async<Result<'a, 'c>>) -> async {
let! twoTrackInput = asyncTwoTrackInput
match twoTrackInput with
| Ok s -> return! switchFunction s
| Error err -> return Error err
}
编辑 2 基于 F# 实现的 bind
let bind3 (binder : 'a -> Async<Result<'b, 'c>>) (asyncResult : Async<Result<'a, 'c>>) = async {
let! result = asyncResult
match result with
| Error e -> return Error e
| Ok x -> return! binder x
}
看看 Suave source code,特别是 WebPart.bind
函数。在 Suave 中,WebPart 是一个接受上下文("context" 是当前请求和到目前为止的响应)和 return 类型 Async<context option>
的结果的函数。将这些链接在一起的语义是,如果 async returns None
,则跳过下一步;如果它 returns Some value
,则以 value
作为输入调用下一步。这与 Result
类型的语义几乎相同,因此您几乎可以复制 Suave 代码并针对结果而不是选项调整它。例如,像这样:
module AsyncResult
let bind (f : 'a -> Async<Result<'b, 'c>>) (a : Async<Result<'a, 'c>>) : Async<Result<'b, 'c>> = async {
let! r = a
match r with
| Ok value ->
let next : Async<Result<'b, 'c>> = f value
return! next
| Error err -> return (Error err)
}
let compose (f : 'a -> Async<Result<'b, 'e>>) (g : 'b -> Async<Result<'c, 'e>>) : 'a -> Async<Result<'c, 'e>> =
fun x -> bind g (f x)
let (>>=) a f = bind f a
let (>=>) f g = compose f g
现在你可以这样写你的链了:
let chain = create >=> update >=> upload >=> publish
let result = chain(schemaObject) |> Async.RunSynchronously
match result with
| Ok article -> Debug.WriteLine(article.name)
| Error error -> Debug.WriteLine(error.code + ":" + error.message)
警告:我无法通过 运行 在 F# Interactive 中验证此代码,因为我没有您的 create/update/etc 的任何示例。职能。原则上它应该可以工作——所有类型都像乐高积木一样组合在一起,这就是你如何判断 F# 代码可能是正确的——但如果我犯了编译器会发现的拼写错误,我还没有了解它。让我知道这是否适合你。
更新: 在评论中,您询问是否需要同时定义 >>=
和 >=>
运算符,并提到您没有在 chain
代码中看不到它们的使用。我定义两者是因为它们有不同的用途,就像 |>
和 >>
运算符有不同的用途一样。 >>=
类似于 |>
:它将 值 传递给 函数 。虽然 >=>
类似于 >>
:它采用 两个函数 并将它们组合在一起。如果您要在 non-AsyncResult 上下文中编写以下内容:
let chain = step1 >> step2 >> step3
然后转换为:
let asyncResultChain = step1AR >=> step2AR >=> step3AR
我使用 "AR" 后缀来指示 return 和 Async<Result<whatever>>
类型的那些函数的版本。另一方面,如果您以 pass-the-data-through-the-pipeline 风格编写:
let result = input |> step1 |> step2 |> step3
那么这将转化为:
let asyncResult = input >>= step1AR >>= step2AR >>= step3AR
所以这就是为什么你需要 bind
和 compose
函数,以及对应于它们的运算符:这样你就可以得到 |>
或>>
个用于 AsyncResult 值的运算符。
顺便说一句,我选的运算符"names"(>>=
和>=>
),我不是随便选的。这些是标准运算符,用于对 Async、Result 或 AsyncResult 等值进行 "bind" 和 "compose" 操作。因此,如果您要定义自己的名称,请坚持使用 "standard" 运算符名称,这样阅读您的代码的其他人就不会感到困惑。
更新 2:阅读这些类型签名的方法如下:
'a -> Async<Result<'b, 'c>>
这是一个采用类型 A 的函数,并且 return 是一个 Async
环绕 Result
。 Result
成功案例为类型 B,失败案例为类型 C。
Async<Result<'a, 'c>>
这是一个值,不是函数。它是 Async
包裹着 Result
,其中类型 A 是成功案例,类型 C 是失败案例。
所以bind
函数有两个参数:
- 从 A 到异步(B 或 C)的函数。
- 一个异步值(A 或 C))。
它 returns:
- 一个异步值(B 或 C)。
查看这些类型签名,您已经可以开始了解 bind
函数的作用。它将采用 A 或 C 的值,并且 "unwrap" 它。如果它是 C,它将产生一个 "either B or C" 值,它是 C(并且不需要调用该函数)。如果是 A,那么为了将其转换为 "either B or C" 值,它将调用 f
函数(接受 A)。
所有这些都发生在异步上下文中,这给类型增加了一层额外的复杂性。如果您查看不涉及异步的 basic version of Result.bind
,可能更容易掌握所有这些内容:
let bind (f : 'a -> Result<'b, 'c>) (a : Result<'a, 'c>) =
match a with
| Ok val -> f val
| Error err -> Error err
在此代码段中,val
的类型为 'a
,err
的类型为 'c
。
最终更新:聊天会话中有一条评论我认为值得保留在答案中(因为人们几乎从不关注聊天链接)。 Developer11 问,
... if I were to ask you what
Result.bind
in my example code maps to your approach, can we rewrite it ascreate >> AsyncResult.bind update
? It worked though. Just wondering i liked the short form and as you said they have a standard meaning? (in haskell community?)
我的回复是:
Yes. If the
>=>
operator is properly written, thenf >=> g
will always be equivalent tof >> bind g
. In fact, that's precisely the definition of thecompose
function, though that might not be immediately obvious to you becausecompose
is written asfun x -> bind g (f x)
rather than asf >> bind g
. But those two ways of writing the compose function would be exactly equivalent. It would probably be very instructive for you to sit down with a piece of paper and draw out the function "shapes" (inputs & outputs) of both ways of writing compose.
为什么要在这里使用面向铁路的编程?如果您只想 运行 一系列操作和 return 有关发生的第一个异常的信息,那么 F# 已经为此使用异常提供了语言支持。为此,您不需要面向铁路的编程。只需将您的 Error
定义为例外:
exception Error of code:string * message:string
修改代码以抛出异常(另请注意,您的 create
函数使用 article
但未使用它,因此我将其删除):
let create () = async {
let ds = new DataContractJsonSerializer(typeof<Error>)
let request = WebRequest.Create("http://example.com") :?> HttpWebRequest
request.Method <- "GET"
try
use response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
return ds.ReadObject(memoryStream) :?> Article
with
| :? WebException as e ->
use reader = new StreamReader(e.Response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
return raise (Error (ds.ReadObject(memoryStream) :?> Error)) }
然后您可以使用 let!
在 async
块中对它们进行排序并添加异常处理来组合函数:
let main () = async {
try
let! created = create ()
let! updated = update created
let! uploaded = upload updated
Debug.WriteLine(uploaded.name)
with Error(code, message) ->
Debug.WriteLine(code + ":" + message) }
如果您想要更复杂的异常处理,那么面向铁路的编程可能会有用,并且肯定有一种方法可以将它与 async
集成,但是如果您只想做您在问题中描述的事情,那么您只需使用标准 F# 就可以更轻松地做到这一点。