异步操作的铁路导向编程 [英] Railway oriented programming with Async operations

查看:57
本文介绍了异步操作的铁路导向编程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以前曾问过类似的问题,但不知何故我找不到出路,请再举一个例子。



作为开始点(略微修剪)的代码可在 https://ideone.com/zkQcIU



(识别 Microsoft.FSharp.Core.Result 类型,但不确定为什么有一些问题)



基本上,所有操作都必须与上一个函数一起进行流水线处理,将结果馈送到下一个函数。这些操作必须是异步的,并且在发生异常的情况下应将错误返回给调用方。



要求是给调用方结果还是错误。所有函数都返回一个元组,该元组填充有成功 类型的文章失败,具有类型的错误对象,该对象具有从服务器返回的描述性代码消息



将感谢我的代码中为被叫方和应答方提供一个可行的示例。



被叫方代码

  type文章= {
名称:string
}

类型错误= {
代码:字符串
消息:字符串
}

让我们创建(文章:文章):结果<文章,错误> =
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)

| :? WebException作为e->
use reader = new StreamReader(e.Response.GetResponseStream())
use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd()))
错误((new DataContractJsonSerializer (typeof< Error>))。ReadObject(memoryStream):?>错误)

其余的链式方法-相同的签名和相似的主体。实际上,您可以重用 create 的正文进行更新上传,然后发布即可测试和编译代码。

 让更新(文章:文章):结果<文章,错误> 
//正文(与创建相同,方法<-PUT)

让上传(文章:文章):结果<文章,错误>
//正文(与create相同,方法<-PUT)

让其发布(文章:文章):结果<文章,错误>
//正文(与create相同,方法< POST)

呼叫者代码

 让链=创建>> Result.bind更新>> Result.bind上传>> Result.bind与
发布
匹配链(schemaObject)|好的文章-> Debug.WriteLine(article.name)
|错误错误-> Debug.WriteLine(错误代码+: +错误消息)

编辑



根据答案并将其与Scott的实现相匹配( https://i.stack.imgur.com/bIxpD.png ),以帮助进行比较和更好地理解。

  let bind2(switchFunction:'a-> Async< Result<'b,'c>>)= 
fun(asyncTwoTrackInput:Async< Result<'a,' c>>)->异步{
让! twoTrackInput = asyncTwoTrackInput
将twoTrackInput与
匹配|好吧->返回! switchFunction s
|错误err->返回错误err
}

编辑2 基于F#实现

  let bind3(binder:'a-> Async<'b,'c>>)( asyncResult:Async< Result<'a,'c>>)=异步{
let!结果= asyncResult

匹配的结果|错误e->返回错误e
|好的x->返回!活页夹x
}


解决方案

看看保存源代码,尤其是 WebPart.bind 函数。在Suave中,WebPart是一个具有上下文的函数(到目前为止,上下文是当前请求和响应),并返回 Async< context option> 。将它们链接在一起的语义是,如果异步返回 None ,则跳过下一步;如果返回某些值,则下一步将以 value 作为输入。这与 Result 类型的语义几乎相同,因此您几乎可以复制Suave代码并针对Result而不是Option进行调整。例如,如下所示:

  module AsyncResult 

let bind(f:'a-> Async< Result< b,< b> c>)(a:Async< Result< a> a,c>)):Async< Result< b< c>> =异步{
放开! r =与

匹配|确定值->
接下来:Async< Result<’b,’c>> = f值
回报!下一个
|错误err-> return(Error err)
}

let compose(f:'a-> Async< Result<'b,'e>>)(g:'b-> Async< ; Result<'c,'e>>):'a->异步<结果<’c,’e>> =
fun x->绑定g(fx)

let(>> =)af =绑定fa
let(> =>)fg =撰写fg

现在,您可以按以下方式编写链:

 让链=创建> =>更新> =>上传> =>发布
let result = chain(schemaObject)|>
与Async.RunSynchronously
的匹配结果|好的文章-> Debug.WriteLine(article.name)
|错误错误-> Debug.WriteLine(error.code +: + error.message)

警告:我没有由于我没有您的create / update / etc的任何示例,因此无法通过在F#Interactive中运行它来验证此代码。职能。从原理上讲,它应该工作—所有类型都像乐高积木一样组合在一起,这可以使您知道F#代码可能是正确的—但是,如果我作了一个错字,那就是编译器可能会抓到,但我还没有知道这一点。让我知道这是否适合您。



更新:在一条评论中,您询问是否需要同时使用>> = 定义了运算符,并提到您没有在<$中看到它们c $ c> chain 代码。我之所以定义两者,是因为它们具有不同的用途,就像 |> > 运算符具有不同的用途一样。 >> = 就像 |> :它将 value 传递给功能。虽然> => 就像>> 一样:它需要两个函数并结合起来如果要在非AsyncResult上下文中编写以下内容:

  let chain = step1>步骤2>> step3 

然后将其翻译为:

 让asyncResultChain = step1AR> => step2AR> => step3AR 

在这里,我使用 AR后缀来表示那些返回 Async< Result< whatever>> 类型。另一方面,如果您是以通过管道传递数据的方式编写的,则:

  let结果=输入|>步骤1 |> step2 |> step3 

然后将其转换为:

  let asyncResult =输入>> = step1AR>> = step2AR>> = step3AR 

这就是为什么同时需要 bind compose 函数的原因对应于它们的运算符:这样您就可以拥有 |> > 您的AsyncResult值的运算符。



BTW,我选择的运算符名称(> == > => ),我没有随机选择。这些是标准操作符,这些操作符在Async或Result或AsyncResult之类的值上各处都用于 bind和 compose操作。因此,如果要定义自己的名称,请坚持使用标准运算符名称,其他人不会混淆您的代码。



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

 'a->异步<结果<’b,’c>> 

此函数采用类型A,并返回 Async 包裹在结果中。 Result 的成功案例为B型,失败案例为C型。

  Async< Result<'a,'c>> 

这是一个值,而不是一个函数。这是一个 Async 包裹在 Result 中,其中类型A是成功案例,类型C是失败案例。 / p>

因此 bind 函数采用两个参数:




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

  • 一个与(A或C)异步的值。


并返回:




  • 一个值是(B或C)异步。



看看这些类型签名,您已经可以开始了解 bind 函数即可。它将采用A或C值,然后解包。如果是C,它将产生一个C的 B或C值(并且不需要调用该函数)。如果它是A,则为了将其转换为 B或C值,它将调用 f 函数(需要A)。



所有这些都发生在异步上下文中,这为类型增加了一层额外的复杂性。如果您查看 Result.bind 的基本版本,不涉及异步:

  let bind(f:'a-> Result<'b,'c>)=(b:Result<'a,'c>)= 
将a与
匹配|好的val-> f val
|错误err->错误err

在此代码段中, val 'a ,而 err 的类型是'c



最终更新:聊天过程中有一条我认为值得保留的评论(因为人们几乎永远不要点击聊天链接)。 Developer11问,


...如果我要问你什么 Result.bind 在我的示例代码中映射到您的方法,是否可以将其重写为 create>> AsyncResult.bind更新?虽然有效。只是想知道我喜欢这种简写形式,正如您所说的那样,它们具有标准含义吗? (在haskell社区中?)


我的回复是:


是。如果> => 运算符正确编写,则 f> => g 始终将等同于 f>>绑定g 。实际上,这正是 compose 函数的定义,尽管这对您可能不是立即显而易见的,因为 compose 是写为 fun x->绑定g(f x)而不是 f>>绑定g 。但是这两种编写compose函数的方式是完全等效的。坐下来坐在一张纸上,画出两种书写方式的功能形状(输入和输出),可能对您很有启发性。



Previously asked similar question but somehow I'm not finding my way out, attempting again with another example.

The code as a starting point (a bit trimmed) is available at https://ideone.com/zkQcIU.

(it has some issue recognizing Microsoft.FSharp.Core.Result type, not sure why)

Essentially all operations have to be pipelined with the previous function feeding the result to the next one. The operations have to be async and they should return error to the caller in case an exception occurred.

The requirement is to give the caller either result or fault. All functions return a Tuple populated with either Success type Article or Failure with type Error object having descriptive code and message returned from the server.

Will appreciate a working example around my code both for the callee and the caller in an answer.

Callee Code

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)

Rest of the chained methods - Same signature and similar bodies. You can actually reuse the body of create for update, upload, and publish to be able to test and compile code.

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)

Caller Code

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)

Edit

Based on the answer and matching it with Scott's implementation (https://i.stack.imgur.com/bIxpD.png), to help in comparison and in better understanding.

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
    }  

Edit 2 Based on F# implementation of 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
}

解决方案

Take a look at the Suave source code, and specifically the WebPart.bind function. In Suave, a WebPart is a function that takes a context (a "context" is the current request and the response so far) and returns a result of type Async<context option>. The semantics of chaining these together are that if the async returns None, the next step is skipped; if it returns Some value, the next step is called with value as the input. This is pretty much the same semantics as the Result type, so you could almost copy the Suave code and adjust it for Result instead of Option. E.g., something like this:

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

Now you can write your chain as follows:

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)

Caution: I haven't been able to verify this code by running it in F# Interactive, since I don't have any examples of your create/update/etc. functions. It should work, in principle — the types all fit together like Lego building blocks, which is how you can tell that F# code is probably correct — but if I've made a typo that the compiler would have caught, I don't yet know about it. Let me know if that works for you.

Update: In a comment, you asked whether you need to have both the >>= and >=> operators defined, and mentioned that you didn't see them used in the chain code. I defined both because they serve different purposes, just like the |> and >> operators serve different purposes. >>= is like |>: it passes a value into a function. While >=> is like >>: it takes two functions and combines them. If you would write the following in a non-AsyncResult context:

let chain = step1 >> step2 >> step3

Then that translates to:

let asyncResultChain = step1AR >=> step2AR >=> step3AR

Where I'm using the "AR" suffix to indicate versions of those functions that return an Async<Result<whatever>> type. On the other hand, if you had written that in a pass-the-data-through-the-pipeline style:

let result = input |> step1 |> step2 |> step3

Then that would translate to:

let asyncResult = input >>= step1AR >>= step2AR >>= step3AR

So that's why you need both the bind and compose functions, and the operators that correspond to them: so that you can have the equivalent of either the |> or the >> operators for your AsyncResult values.

BTW, the operator "names" that I picked (>>= and >=>), I did not pick randomly. These are the standard operators that are used all over the place for the "bind" and "compose" operations on values like Async, or Result, or AsyncResult. So if you're defining your own, stick with the "standard" operator names and other people reading your code won't be confused.

Update 2: Here's how to read those type signatures:

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

This is a function that takes type A, and returns an Async wrapped around a Result. The Result has type B as its success case, and type C as its failure case.

Async<Result<'a, 'c>>

This is a value, not a function. It's an Async wrapped around a Result where type A is the success case, and type C is the failure case.

So the bind function takes two parameters:

  • a function from A to an async of (either B or C)).
  • a value that's an async of (either A or C)).

And it returns:

  • a value that's an async of (either B or C).

Looking at those type signatures, you can already start to get an idea of what the bind function will do. It will take that value that's either A or C, and "unwrap" it. If it's C, it will produce an "either B or C" value that's C (and the function won't need to be called). If it's A, then in order to convert it to an "either B or C" value, it will call the f function (which takes an A).

All this happens within an async context, which adds an extra layer of complexity to the types. It might be easier to grasp all this if you look at the basic version of Result.bind, with no async involved:

let bind (f : 'a -> Result<'b, 'c>) (a : Result<'a, 'c>) =
    match a with
    | Ok val -> f val
    | Error err -> Error err

In this snippet, the type of val is 'a, and the type of err is 'c.

Final update: There was one comment from the chat session that I thought was worth preserving in the answer (since people almost never follow chat links). Developer11 asked,

... 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?)

My reply was:

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.

这篇关于异步操作的铁路导向编程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆