使用异步操作的铁路导向编程

时间:2018-03-21 06:17:18

标签: asynchronous error-handling f# function-composition

以前问了类似的问题,但不知怎的,我没有找到出路,再次尝试另一个例子。

https://ideone.com/zkQcIU提供了作为起点(有点修剪)的代码。

(它有一些问题识别Microsoft.FSharp.Core.Result类型,不确定原因)

基本上所有操作都必须使用前一个函数进行流水线处理,然后将结果提供给下一个。操作必须是异步的,如果发生异常,它们应该向调用者返回错误。

要求是给呼叫者带来结果或错误。所有函数都会返回一个元组,其中包含成功 type Article失败,其中type Error对象具有描述性codemessage从服务器返回。

我会在答案中了解我的代码中的被调用者和调用者的工作示例。

被叫代码

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)

其他链式方法 - 相同的签名和类似的主体。您实际上可以为createupdateupload重用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
}

2 个答案:

答案 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

这就是为什么你需要bindcompose函数以及与它们对应的运算符:这样你就可以拥有|>或{{ 1}} AsyncResult值的运算符。

BTW,我选择的运营商“名字”(>>>>=),我没有随机挑选。这些是遍及整个地方的标准运算符,用于对Async,Result或AsyncResult等值进行“绑定”和“组合”操作。因此,如果您要定义自己的名称,请坚持使用“标准”操作员名称,其他人阅读您的代码时不会感到困惑。

更新2 :以下是阅读这些类型签名的方法:

>=>

这是一个接受类型A的函数,并返回'a -> Async<Result<'b, 'c>> 围绕AsyncResult将B类作为其成功案例,并将C作为其失败案例。

Result

这是一个值,而不是一个函数。这是Async<Result<'a, 'c>> 缠绕Async,其中类型A是成功案例,类型C是失败案例。

因此Result函数有两个参数:

  • 从A到异步(B或C)的函数。)
  • 是(A或C)异步的值。)

它返回:

  • 与(B或C)异步的值。

查看这些类型的签名,您已经可以开始了解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)) }

然后你可以通过使用asynclet!块中对它们进行排序来编写函数并添加异常处理:

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#即可轻松完成。