Dev*_*r11 7 error-handling f# asynchronous function-composition
以前问了类似的问题,但不知怎的,我没有找到出路,再次尝试另一个例子.
可以在https://ideone.com/zkQcIU上找到代码作为起点(稍微修整一下).
(它有一些问题识别Microsoft.FSharp.Core.Result类型,不知道为什么)
基本上所有操作都必须通过前一个函数进行流水线操作,将结果输送到下一个操作.操作必须是异步的,如果发生异常,它们应该向调用者返回错误.
要求是给调用者结果或错误.所有函数返回填充任何一个元组的成功 type Article或失败有type Error其描述对象code,并message从服务器返回.
我将在一个答案中欣赏我的代码中的被调用者和调用者的工作示例.
被叫代码
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)
Run Code Online (Sandbox Code Playgroud)
其余的链式方法 - 相同的签名和类似的主体.实际上,你可以重复使用的身体create的update,upload以及publish能够测试和编译代码.
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)
Run Code Online (Sandbox Code Playgroud)
来电代码
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)
Run Code Online (Sandbox Code Playgroud)
编辑
根据答案并将其与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
}
Run Code Online (Sandbox Code Playgroud)
编辑2基于绑定的F#实现
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
}
Run Code Online (Sandbox Code Playgroud)
看看Suave源代码,特别是WebPart.bind函数.在Suave中,WebPart是一个接受上下文的函数("上下文"是当前请求和到目前为止的响应)并返回类型的结果Async<context option>.将这些链接在一起的语义是,如果异步返回None,则跳过下一步; 如果它返回Some value,则调用下一步value作为输入.这几乎与Result类型的语义相同,因此您几乎可以复制Suave代码并将其调整为Result而不是Option.例如,像这样:
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
Run Code Online (Sandbox Code Playgroud)
现在您可以按如下方式编写链:
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)
Run Code Online (Sandbox Code Playgroud)
警告:我无法通过在F#Interactive中运行此代码来验证此代码,因为我没有您的create/update/etc的任何示例.功能.原则上它应该工作 - 所有类型都像Lego构建块一样,这就是你可以告诉F#代码可能是正确的 - 但如果我做了编译器会捕获的拼写错误,我还没有了解它.如果这对您有用,请告诉我.
更新:在评论中,您询问是否需要同时定义>>=和>=>运算符,并提到您没有在chain代码中看到它们.我定义了两者,因为它们用于不同的目的,就像|>和>>运算符用于不同的目的一样.>>=就像|>:它将值传递给函数.虽然>=>如此>>:它需要两个功能并将它们组合在一起.如果要在非AsyncResult上下文中编写以下内容:
let chain = step1 >> step2 >> step3
Run Code Online (Sandbox Code Playgroud)
然后转换为:
let asyncResultChain = step1AR >=> step2AR >=> step3AR
Run Code Online (Sandbox Code Playgroud)
我使用"AR"后缀来表示返回Async<Result<whatever>>类型的那些函数的版本.另一方面,如果您在传递数据通过管道样式中写道:
let result = input |> step1 |> step2 |> step3
Run Code Online (Sandbox Code Playgroud)
然后,这将转化为:
let asyncResult = input >>= step1AR >>= step2AR >>= step3AR
Run Code Online (Sandbox Code Playgroud)
所以这就是为什么你需要同时bind和compose功能,以及与它们对应的运营商:让你可以有相当于要么|>或>>运营商为您AsyncResult值.
顺便说一句,操作员"命名"我选择(>>=和>=>),我没有随机挑选.这些是遍及整个地方的标准运算符,用于对Async,Result或AsyncResult等值进行"绑定"和"组合"操作.因此,如果您要定义自己的名称,请坚持使用"标准"操作员名称,其他人阅读您的代码时不会混淆.
更新2:以下是如何阅读这些类型签名:
'a -> Async<Result<'b, 'c>>
Run Code Online (Sandbox Code Playgroud)
这是一个接受类型A的函数,并返回一个Async包裹着的函数Result.将ResultB类作为其成功案例,将类型C作为其失败案例.
Async<Result<'a, 'c>>
Run Code Online (Sandbox Code Playgroud)
这是一个值,而不是一个函数.它是一个Async包含Result在类型A是成功案例的地方,而类型C是失败案例.
所以该bind函数有两个参数:
它返回:
查看这些类型的签名,您已经可以开始了解该bind函数的功能.它将采用该值为A或C,并"解包"它.如果它是C,它将产生一个"B或C"值,即C(并且不需要调用该函数).如果是A,则为了将其转换为"B或C"值,它将调用该f函数(采用A).
所有这些都发生在异步上下文中,这为类型增加了额外的复杂性.如果你看一下没有异步的基本版本,Result.bind可能会更容易掌握所有这些:
let bind (f : 'a -> Result<'b, 'c>) (a : Result<'a, 'c>) =
match a with
| Ok val -> f val
| Error err -> Error err
Run Code Online (Sandbox Code Playgroud)
在此片段中,valis 'a的类型和erris 的类型'c.
最后更新:聊天会话中有一条评论我认为值得在答案中保留(因为人们几乎从不关注聊天链接).Developer11问道,
...如果我问你的
Result.bind示例代码中的哪些代码映射到您的方法,我们可以将其重写为create >> AsyncResult.bind update?虽然有用.只是想知道我喜欢简短的形式,正如你所说他们有一个标准的含义?(在haskell社区?)
我的回答是:
是.如果
>=>运营商编写正确,那么f >=> g将永远等同于f >> bind g.事实上,这正是compose函数的定义,尽管这可能不会立即显而易见,因为它compose是作为fun x -> bind g (f x)而不是作为f >> bind g.但是这两种编写函数的方式完全相同.你可以用一张纸坐下来绘制两种写作方式的"形状"(输入和输出)功能,这对你很有帮助.