Railway oriented programming with Async operations

て烟熏妆下的殇ゞ 提交于 2019-12-10 03:41:37

问题


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
}

回答1:


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.




回答2:


Why do you want to use Railway Oriented Programming here? If you just want to run a sequence of operations and return information about the first exception that occurs, then F# already provides a language support for this using exceptions. You do not need Railway Oriented Programming for this. Just define your Error as an exception:

exception Error of code:string * message:string

Modify the code to throw the exception (also note that your create function takes article but does not use it, so I deleted that):

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

And then you can compose functions just by sequencing them in async block using let! and add exception handling:

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

If you wanted more sophisticated exception handling, then Railway Oriented Programming might be useful and there is certainly a way of integrating it with async, but if you just want to do what you described in your question, then you can do that much more easily with just standard F#.



来源:https://stackoverflow.com/questions/49399474/railway-oriented-programming-with-async-operations

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!