我正在编写一个异步HTTP API客户端模块/库。为了使一切尽可能DRY,我试图从单独的部分组成每个HTTP API调用,使API调用自下而上:构建请求,获得响应,将响应读取到字符串缓冲区,解析该字符串缓冲区的JSON内容为对象。
到目前为止,我有这样的代码:module ApiUtils =
// ... request builder fns omitted ...
let getResponse<'a> (request : Net.WebRequest) =
request.AsyncGetResponse()
let readResponse (response : Net.WebResponse) =
use reader = new StreamReader(response.GetResponseStream())
reader.AsyncReadToEnd()
let getString = getResponse >> (Async.flatMap readResponse)
let parseJson<'T> responseText : 'T =
Json.JsonConvert.DeserializeObject<'T> responseText
let getJson<'T> = getString >> (Async.map parseJson<'T>)
并且,正如您所看到的,我已经用我自己的添加扩展了Async模块:
module Async =
let map f m =
async {
let! v = m
return f v
}
let flatMap f m =
async {
let! v = m
return! f v
}
我想要实现的目标是建立一个模块,我可以在async
块中使用的函数,以利用计算表达式语法的所有优势。我想知道我是否做得对,我是否选择了正确的名字,等等。我几乎没有受过正规的函数式编程教育,有时我甚至不确定我知道我在做什么。
在f#中编写异步代码时,我发现使用内置的计算异步工作流语法比尝试构建更复杂的组合子更容易。
在你的例子中,你不会真的重复任何代码,如果你只是写一个简单的函数,所以我认为以下不会打破DRY原则,它是相当简单的(也很容易扩展代码来处理异常,否则会很困难):
let getJson<'T> (request:Net.WebRequest) = async {
let! response = request.AsyncGetResponse()
use reader = new StreamReader(response.GetResponseStream())
let! data = reader.AsyncReadToEnd()
return Json.JsonConvert.DeserializeObject<'T> data }
当然,如果您需要在代码的其他地方下载数据用于其他目的,您可以将代码分成downloadData
和getJson
。
通常,在编写函数式代码时,有两种选择如何组合计算:
使用语言语法(如循环,
let
和try .. with
,use
在纯f#和异步工作流中)。如果你正在编写一些计算,这种方法通常会很好地工作,因为语言被设计为描述计算,并且可以很好地完成。使用自定义组合子(如
map
、>>
和|>
或特定于库的操作符)。如果你正在建模的东西不仅仅是一个计算,例如交互式动画、股票期权、用户界面组件或解析器,这是需要的。然而,只有当语言的基本特性不足以表达问题时,我才会遵循这条路径。