以前问了类似的问题,但不知怎的,我没有找到出路,再次尝试另一个例子。
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)
其他链式方法 - 相同的签名和类似的主体。您实际上可以为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)
来电者代码
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#实现
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
}
答案 0 :(得分:7)
查看Suave source code,特别是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
现在您可以按如下方式编写链:
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的任何示例。功能。原则上它应该工作 - 所有类型都像Lego构建块一样,这就是你可以告诉F#代码可能是正确的 - 但如果我做了编译器会捕获的拼写错误,我还没有了解它。如果这对您有用,请告诉我。
更新:在评论中,您询问是否需要定义>>=
和>=>
运算符,并提到您没有看到它们被用于chain
代码。我之所以定义它们是因为它们用于不同目的,就像|>
和>>
运算符用于不同目的一样。 >>=
与|>
类似:它将值传递给函数。虽然>=>
与>>
类似:但它需要两个函数并将它们组合在一起。如果您要在非AsyncResult上下文中编写以下内容:
let chain = step1 >> step2 >> step3
然后转换为:
let asyncResultChain = step1AR >=> step2AR >=> step3AR
我使用“AR”后缀来指示返回Async<Result<whatever>>
类型的那些函数的版本。另一方面,如果你用传递数据通过管道样式写了:
let result = input |> step1 |> step2 |> step3
然后那将转化为:
let asyncResult = input >>= step1AR >>= step2AR >>= step3AR
这就是为什么你需要bind
和compose
函数以及与它们对应的运算符:这样你就可以拥有|>
或{{ 1}} AsyncResult值的运算符。
BTW,我选择的运营商“名字”(>>
和>>=
),我没有随机挑选。这些是遍及整个地方的标准运算符,用于对Async,Result或AsyncResult等值进行“绑定”和“组合”操作。因此,如果您要定义自己的名称,请坚持使用“标准”操作员名称,其他人阅读您的代码时不会感到困惑。
更新2 :以下是阅读这些类型签名的方法:
>=>
这是一个接受类型A的函数,并返回'a -> Async<Result<'b, 'c>>
围绕Async
。 Result
将B类作为其成功案例,并将C作为其失败案例。
Result
这是一个值,而不是一个函数。这是Async<Result<'a, 'c>>
缠绕Async
,其中类型A是成功案例,类型C是失败案例。
因此Result
函数有两个参数:
它返回:
查看这些类型的签名,您已经可以开始了解bind
函数将执行的操作。它将采用该值为A或C,并“解开”它。如果它是C,它将产生一个“B或C”值,即C(并且不需要调用该函数)。如果是A,那么为了将其转换为“B或C”值,它将调用bind
函数(采用A)。
所有这些都发生在异步上下文中,这为类型增加了额外的复杂性。如果你看一下basic version of Result.bind
,没有涉及异步,可能会更容易理解这一切:
f
在此代码段中,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
。
最终更新:聊天会话中有一条评论我认为值得保留在答案中(因为人们几乎从不关注聊天链接)。 Developer11问,
...如果我问你的示例代码中的
'c
映射到您的方法,我们可以将其重写为Result.bind
吗?虽然有用。只是想知道我喜欢简短的形式,正如你所说他们有一个标准的含义? (在haskell社区?)
我的回复是:
是。如果
create >> AsyncResult.bind update
运算符已正确编写,则>=>
始终将等同于f >=> g
。事实上,这正是f >> bind g
函数的定义,尽管这可能不会立即显而易见,因为compose
被写为compose
而不是fun x -> bind g (f x)
。但是这两种编写函数的方法是完全等效。你可以用一张纸坐下来绘制两种写作方式的“形状”(输入和输出)功能,这对你来说非常有启发性。
答案 1 :(得分:4)
为什么要在这里使用铁路定向编程?如果您只想运行一系列操作并返回有关发生的第一个异常的信息,那么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)) }
然后你可以通过使用async
在let!
块中对它们进行排序来编写函数并添加异常处理:
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#即可轻松完成。