[英]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# Async
和Result
类型来表示每个 API 调用的结果。 然后,您可以使用这些类型的bind
函数来构建一个工作流,在该工作流中,您仅在之前的调用成功时继续处理。 为了使这更容易,您可以将Async<Result<_,_>>
您将要使用的Async<Result<_,_>>
包装在其自己的类型中,并围绕binding
这些结果构建一个模块以编排链式计算。 下面是一个简单的例子:
首先,我们将布局ApiCallResult
类型来包装Async
和Result
,我们将定义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
实例,允许我们执行诸如bind
、 map
和return
以便我们可以处理计算结果并将它们提供给下一个计算结果。
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.