简体   繁体   English

F# 中是否有链接计算的方法?

[英]Is there a way in F# to chain computation?

I would like to create a chain of expressions and any of them can fail when the computation should just stop.我想创建一个表达式链,当计算应该停止时,它们中的任何一个都可能失败。

With Unix pipes it is usually like this:对于 Unix 管道,它通常是这样的:

bash-3.2$ echo && { echo 'a ok'; echo; }  && { echo 'b ok'; echo; }
a ok

b ok

When something fails the pipeline stops:当出现故障时,管道会停止:

echo && { echo 'a ok'; false; }  && { echo 'b ok'; echo; }

a ok

I can handle Optionals but my problem is that I might want to do multiple things in each branch:我可以处理 Optionals 但我的问题是我可能想在每个分支中做多件事:

let someExternalOperation = callToAnAPI()
match someExternalOperation with
| None -> LogAndStop()
| Some x -> LogAndContinue()

Then I would like to keep going with other API calls and only stop if there is an error.然后我想继续进行其他 API 调用,只有在出现错误时才停止。

Is there something like that in F#?在 F# 中有类似的东西吗?

Update1:更新1:

What I am trying to do is calling out to external APIs.我想要做的是调用外部 API。 Each call can fail.每次调用都可能失败。 Would be nice to try to retry but not required.尝试重试会很好,但不是必需的。

You can use the F# Async and Result types together to represent the results of each API Call.您可以结合使用 F# AsyncResult类型来表示每个 API 调用的结果。 You can then use the bind functions for those types to build a workflow in which you only continue processing when the previous calls were successful.然后,您可以使用这些类型的bind函数来构建一个工作流,在该工作流中,您仅在之前的调用成功时继续处理。 In order to make that easier, you can wrap the Async<Result<_,_>> you would be working with for each api call in its own type and build a module around binding those results to orchestrate a chained computation.为了使这更容易,您可以将Async<Result<_,_>>您将要使用的Async<Result<_,_>>包装在其自己的类型中,并围绕binding这些结果构建一个模块以编排链式计算。 Here's a quick example of what that would look like:下面是一个简单的例子:

First, we would lay out the type ApiCallResult to wrap Async and Result , and we would define ApiCallError to represent HTTP error responses or exceptions:首先,我们将布局ApiCallResult类型来包装AsyncResult ,我们将定义ApiCallError来表示 HTTP 错误响应或异常:

open System
open System.Net
open System.Net.Http

type ApiCallError =
| HttpError of (int * string)
| UnexpectedError of exn

type ApiCallResult<'a> = Async<Result<'a, ApiCallError>>

Next, we would create a module to work with ApiCallResult instances, allowing us to do things like bind , map , and return so that we can process the results of a computation and feed them into the next one.接下来,我们将创建一个模块来处理ApiCallResult实例,允许我们执行诸如bindmapreturn以便我们可以处理计算结果并将它们提供给下一个计算结果。

module ApiCall =
    let ``return`` x : ApiCallResult<_> =
        async { return Ok x }

    let private zero () : ApiCallResult<_> = 
        ``return`` []

    let bind<'a, 'b> (f: 'a -> ApiCallResult<'b>) (x: ApiCallResult<'a>) : ApiCallResult<'b> =
        async {
            let! result = x
            match result with
            | Ok value -> 
                return! f value
            | Error error ->
                return Error error
        }

    let map f x = x |> bind (f >> ``return``)

    let combine<'a> (acc: ApiCallResult<'a list>) (cur: ApiCallResult<'a>) =
        acc |> bind (fun values -> cur |> map (fun value -> value :: values))

    let join results =
        results |> Seq.fold (combine) (zero ())

Then, you would have a module to simply do your API calls, however that works in your real scenario.然后,您将有一个模块来简单地执行 API 调用,但它适用于您的实际场景。 Here's one that just handles GETs with query parameters, but you could make this more sophisticated:这是一个只处理带有查询参数的 GET 的方法,但您可以使其更复杂:

module Api =
    let call (baseUrl: Uri) (queryString: string) : ApiCallResult<string> =
        async {
            try
                use client = new HttpClient()
                let url = 
                    let builder = UriBuilder(baseUrl)
                    builder.Query <- queryString
                    builder.Uri
                printfn "Calling API: %O" url
                let! response = client.GetAsync(url) |> Async.AwaitTask
                let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
                if response.IsSuccessStatusCode then
                    let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
                    return Ok content
                else
                    return Error <| HttpError (response.StatusCode |> int, content)
            with ex ->
                return Error <| UnexpectedError ex
        }

    let getQueryParam name value =
        value |> WebUtility.UrlEncode |> sprintf "%s=%s" name

Finally, you would have your actual business workflow logic, where you call multiple APIs and feed the results of one into another.最后,您将拥有实际的业务工作流逻辑,您可以在其中调用多个 API 并将其中一个的结果提供给另一个。 In the below example, anywhere you see callMathApi , it is making a call to an external REST API that may fail, and by using the ApiCall module to bind the results of the API call, it only proceeds to the next API call if the previous call was successful.在下面的示例中,在任何您看到callMathApi地方,它都会调用可能失败的外部 REST API,并且通过使用ApiCall模块绑定 API 调用的结果,如果前一个 API 调用,它只会继续下一个 API 调用通话成功。 You can declare an operator like >>= to eliminate some of the noise in the code when binding computations together:在将计算绑定在一起时,您可以声明一个像>>=这样的运算符来消除代码中的一些噪音:

module MathWorkflow =
    let private (>>=) x f = ApiCall.bind f x

    let private apiUrl = Uri "http://api.mathjs.org/v4/" // REST API for mathematical expressions

    let private callMathApi expression =
        expression |> Api.getQueryParam "expr" |> Api.call apiUrl

    let average values =        
        values 
        |> List.map (sprintf "%d") 
        |> String.concat "+" 
        |> callMathApi
        >>= fun sum -> 
                sprintf "%s/%d" sum values.Length 
                |> callMathApi

    let averageOfSquares values =
        values 
        |> List.map (fun value -> sprintf "%d*%d" value value)
        |> List.map callMathApi
        |> ApiCall.join
        |> ApiCall.map (List.map int)
        >>= average

This example uses the Mathjs.org API to compute the average of a list of integers (making one API call to compute the sum, then another to divide by the number of elements), and also allows you to compute the average of the squares of a list of values, by calling the API asynchronously for each element in the list to square it, then joining the results together and computing the average.此示例使用 Mathjs.org API 来计算整数列表的平均值(进行一次 API 调用以计算总和,然后再调用一次以除以元素数),并且还允许您计算整数列表的平均值一个值列表,通过为列表中的每个元素异步调用 API 对其进行平方,然后将结果连接在一起并计算平均值。 You can use these functions as follows (I added a printfn to the actual API call so it logs the HTTP requests):您可以按如下方式使用这些函数(我在实际的 API 调用中添加了一个printfn ,以便它记录 HTTP 请求):

Calling average:通话平均值:

MathWorkflow.average [1;2;3;4;5] |> Async.RunSynchronously

Outputs:输出:

Calling API: http://api.mathjs.org/v4/?expr=1%2B2%2B3%2B4%2B5
Calling API: http://api.mathjs.org/v4/?expr=15%2F5
[<Struct>]
val it : Result<string,ApiCallError> = Ok "3"

Calling averageOfSquares:调用averageOfSquares:

MathWorkflow.averageOfSquares [2;4;6;8;10] |> Async.RunSynchronously

Outputs:输出:

Calling API: http://api.mathjs.org/v4/?expr=2*2
Calling API: http://api.mathjs.org/v4/?expr=4*4
Calling API: http://api.mathjs.org/v4/?expr=6*6
Calling API: http://api.mathjs.org/v4/?expr=8*8
Calling API: http://api.mathjs.org/v4/?expr=10*10
Calling API: http://api.mathjs.org/v4/?expr=100%2B64%2B36%2B16%2B4
Calling API: http://api.mathjs.org/v4/?expr=220%2F5
[<Struct>]
val it : Result<string,ApiCallError> = Ok "44"

Ultimately, you may want to implement a custom Computation Builder to allow you to use a computation expression with the let!最终,您可能想要实现一个自定义的计算生成器,以允许您使用let!的计算表达式let! syntax, instead of explicitly writing the calls to ApiCall.bind everywhere.语法,而不是在任何地方显式地编写对ApiCall.bind的调用。 This is fairly simple, since you already do all the real work in the ApiCall module, and you just need to make a class with the appropriate Bind/Return members:这相当简单,因为您已经在ApiCall模块中完成了所有实际工作,您只需要使用适当的 Bind/Return 成员创建一个类:


type ApiCallBuilder () =
    member __.Bind (x, f) = ApiCall.bind f x
    member __.Return x = ApiCall.``return`` x
    member __.ReturnFrom x = x
    member __.Zero () = ApiCall.``return`` ()

let apiCall = ApiCallBuilder()

With the ApiCallBuilder , you could rewrite the functions in the MathWorkflow module like this, making them a little easier to read and compose:使用ApiCallBuilder ,您可以像这样重写MathWorkflow模块中的函数,使它们更易于阅读和编写:

    let average values =        
        apiCall {
            let! sum =
                values 
                |> List.map (sprintf "%d") 
                |> String.concat "+" 
                |> callMathApi

            return! 
                sprintf "%s/%d" sum values.Length
                |> callMathApi
        }        

    let averageOfSquares values =
        apiCall {
            let! squares = 
                values 
                |> List.map (fun value -> sprintf "%d*%d" value value)
                |> List.map callMathApi
                |> ApiCall.join

            return! squares |> List.map int |> average
        }

These work as you described in the question, where each API call is made independently and the results feed into the next call, but if one call fails the computation is stopped and the error is returned.这些工作如您在问题中所描述的那样,其中每个 API 调用都是独立进行的,结果将输入下一次调用,但如果一次调用失败,则计算将停止并返回错误。 For example, if you change the URL used in the example calls here to the v3 API (" http://api.mathjs.org/v3/ ") without changing anything else, you get the following:例如,如果您将示例调用中使用的 URL 更改为v3 API(“ http://api.mathjs.org/v3/ ”)而不更改任何其他内容,您将获得以下内容:

Calling API: http://api.mathjs.org/v3/?expr=2*2
[<Struct>]
val it : Result<string,ApiCallError> =
  Error
    (HttpError
       (404,
        "<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /v3/</pre>
</body>
</html>
"))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM