簡體   English   中英

F# 中是否有鏈接計算的方法?

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

我想創建一個表達式鏈,當計算應該停止時,它們中的任何一個都可能失敗。

對於 Unix 管道,它通常是這樣的:

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

b ok

當出現故障時,管道會停止:

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

a ok

我可以處理 Optionals 但我的問題是我可能想在每個分支中做多件事:

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

然后我想繼續進行其他 API 調用,只有在出現錯誤時才停止。

在 F# 中有類似的東西嗎?

更新1:

我想要做的是調用外部 API。 每次調用都可能失敗。 嘗試重試會很好,但不是必需的。

您可以結合使用 F# AsyncResult類型來表示每個 API 調用的結果。 然后,您可以使用這些類型的bind函數來構建一個工作流,在該工作流中,您僅在之前的調用成功時繼續處理。 為了使這更容易,您可以將Async<Result<_,_>>您將要使用的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 並將其中一個的結果提供給另一個。 在下面的示例中,在任何您看到callMathApi地方,它都會調用可能失敗的外部 REST API,並且通過使用ApiCall模塊綁定 API 調用的結果,如果前一個 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 調用中添加了一個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"

最終,您可能想要實現一個自定義的計算生成器,以允許您使用let!的計算表達式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>
"))

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM