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

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)

其余的链式方法 - 相同的签名和类似的主体.实际上,你可以重复使用的身体createupdate,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)

rmu*_*unn 7

看看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)

所以这就是为什么你需要同时bindcompose功能,以及与它们对应的运营商:让你可以有相当于要么|>>>运营商为您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函数有两个参数:

  • 从A到异步(B或C)的函数.
  • 一个异步的值(A或C)).

它返回:

  • 一个异步的值(B或C).

查看这些类型的签名,您已经可以开始了解该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.但是这两种编写函数的方式完全相同.你可以用一张纸坐下来绘制两种写作方式的"形状"(输入和输出)功能,这对你很有帮助.