F#中是否有一种方法可以链接计算?

时间:2019-12-17 20:37:29

标签: f#

我想创建一个表达式链,当计算应该停止时,它们中的任何一个都会失败。

对于Unix管道,通常是这样的:

public void test_atZone_dstOverlapWinter() {
        OffsetDateTime t = OffsetDateTime.of(2007, 10, 28, 2, 30, 0, 0, OFFSET_PONE);
        assertEquals(t.atZoneSimilarLocal(ZONE_PARIS).toLocalDateTime(), t.toLocalDateTime());
        assertEquals(t.atZoneSimilarLocal(ZONE_PARIS).getOffset(), OFFSET_PONE);
        assertEquals(t.atZoneSimilarLocal(ZONE_PARIS).getZone(), ZONE_PARIS);
    }

当某些事情失败时,管道停止:

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

b ok

我可以处理Optionals,但是我的问题是我可能想在每个分支中做很多事情:

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

a ok

然后,我想继续处理其他API调用,并且仅在出现错误时停止。

F#中是否有类似的东西?

Update1:​​

我想做的是调用外部API。每次通话都会失败。可以尝试重试,但不是必需的。

1 个答案:

答案 0 :(得分:3)

您可以同时使用F#AsyncResult类型来表示每个API调用的结果。然后,您可以对这些类型使用bind函数来构建工作流,在该工作流中,只有在先前的调用成功后才继续处理。为了简化操作,您可以将每个api调用要使用的Async<Result<_,_>>包装成自己的类型,并围绕binding构建一个模块,以将这些结果编排为链式计算。这是一个简短的示例:

首先,我们将布局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>>

接下来,我们将创建一个模块来与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 ())

然后,您将有一个模块可以简单地进行API调用,但是该模块在您的实际情况下有效。这是一种只处理带有查询参数的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

最后,您将拥有实际的业务工作流逻辑,在其中调用多个API,并将一个API的结果提供给另一个API。在下面的示例中,您看到callMathApi的任何地方都在调用可能会失败的外部REST API,并且通过使用ApiCall模块来绑定API调用的结果,它只会继续如果上一个调用成功,则转到下一个API调用。您可以声明类似>>=的运算符,以在将计算绑定在一起时消除代码中的一些干扰:

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

此示例使用Mathjs.org API计算整数列表的平均值(进行一个API调用以计算总和,然后进行另一个API调用除以元素数),还允许您计算整数的平均值。值列表的平方,方法是异步调用列表中每个元素的API以对其求平方,然后将结果结合在一起并计算平均值。您可以按以下方式使用这些功能(我在实际的API调用中添加了printfn,因此它记录了HTTP请求):

通话平均值:

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

输出:

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"

调用averageOfSquares:

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

输出:

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"

最终,您可能想实现一个自定义的Computation Builder,以允许您使用具有let!语法的计算表达式,而不是在任何地方都显式地编写对ApiCall.bind的调用。这非常简单,因为您已经在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()

使用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
        }

这些工作如您在问题中所述,其中每个API调用都是独立进行的,结果将馈送到下一个调用中,但是如果一个调用失败,则计算将停止并返回错误。例如,如果您将示例调用中使用的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>
"))