具有异步操作的面向铁路的编程

Railway oriented programming with Async operations

之前问过类似的问题,但不知何故我找不到出路,再次尝试另一个例子。

可在 https://ideone.com/zkQcIU 获得作为起点的代码(略有修整)。

(它在识别 Microsoft.FSharp.Core.Result 类型时遇到一些问题,不知道为什么)

基本上所有操作都必须通过前一个函数将结果传递给下一个函数进行流水线处理。这些操作必须是异步的,如果发生异常,它们应该 return 向调用者发送错误。

要求是给调用者结果或错误。所有函数 return 填充有 Success type ArticleFailure 的元组 type Error 对象具有描述性 codemessage 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)

其余链接方法 - 相同的签名和相似的主体。实际上,您可以为 updateuploadpublish 重用 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

所以这就是为什么你需要 bindcompose 函数,以及对应于它们的运算符:这样你就可以得到 |>>> 个用于 AsyncResult 值的运算符。

顺便说一句,我选的运算符"names"(>>=>=>),我不是随便选的。这些是标准运算符,用于对 Async、Result 或 AsyncResult 等值进行 "bind" 和 "compose" 操作。因此,如果您要定义自己的名称,请坚持使用 "standard" 运算符名称,这样阅读您的代码的其他人就不会感到困惑。

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

'a -> Async<Result<'b, 'c>>

这是一个采用类型 A 的函数,并且 return 是一个 Async 环绕 ResultResult 成功案例为类型 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 的类型为 'aerr 的类型为 '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# 就可以更轻松地做到这一点。