65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 F# 查询 Last.fm 网络 API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (6投票s)

2017年11月7日

CPOL

3分钟阅读

viewsIcon

13993

这个简单的查询网络 API 的应用程序将向您展示 F# 编程语言的一些好处。

引言

让我们想象一下,您有前卫的音乐品味,所以您只想向您的朋友推荐那些最主流的艺术家。如果您在 last.fm 上有个人资料,那么您可以编写一个小工具来查询和处理您的收听统计数据以自动化此任务。

我用于此任务的工具是 F# 编程语言,我将向您展示它的一些好处,例如类型提供程序或与对象相比更容易的单元测试函数。

完整的源代码可以在 这里 访问。

工作流

任务如下

  1. last.fm 榜单中获取前 50 位艺术家。
  2. 将它们转换为他们的名字的集合。
  3. 删除那些已经被推荐的。
  4. 替换艺术家姓名中的非 URL 字符,以便进行进一步的 API 调用。
  5. 对于每个艺术家,进行额外的 API 调用以获取听众数量。
  6. 将给定信息转换为更简洁的数据类型,其中仅包含艺术家姓名和听众计数。
  7. 按听众数量对艺术家进行排序,这意味着听众更多的艺术家更为主流。

请注意,F# 管道如何以真正地道的风格表达这种工作流程。

let result = getTopArtists
                |> getTopArtistNames
                |> removeAlreadyRecomendedArtists
                |> getUrlEncodedArtistNames 
                |> mapArtistNamesToArtistInfo getArtistInfo
                |> getArtistsShortInfo
                |> orderArtistsByListenersCount          

利用类型提供程序

类型提供程序可以说是 F# 中最受宣传的特性。类型提供程序允许我们访问许多上下文,例如 Web API、数据库模式等,作为强类型实体,这使我们能够获得编译时支持和一些不错的福利,例如 IDE 自动完成。

为了在我们的应用程序中使用它,我们

  1. 导入 FSharp.Data
  2. 声明我们的 API 响应片段
    let [<Literal>] TopArtistsSample = """{  
       "topartists":{  
          "artist":[  
             {  
                "name":"Porcupine Tree",
                //skipped for the sake of breivety
             }
          ],
          "@attr":{  
             "user":"Morbid_soul",
             "page":"1",
             "perPage":"2",
             "totalPages":"165",
             "total":"330"
          }
       }
        }"""
  3. 通过 JsonProvider 从我们的示例构造类型
    type TopArtists = JsonProvider<TopArtistsSample>
  4. 享受强类型响应的编译时支持。

使用高阶函数改进单元测试

让我们仔细看看以下函数

let mapArtistNamesToArtistInfo getArtistInfoFn artists = 
    artists
        |> Array.map (fun i -> getArtistInfoFn i) 

getArtistInfoFn 负责与远程网络 API 交互。以下是这种场景的单元测试的执行方式。

let getArtistInfoStub input = 
        match input with
        | "Nokturanl Mortum" -> 1
        | "Krobak" -> 2
        | _ -> 3

[<Fact>]
let mapArtistNamesToArtistInfo_returns_expected_result() =
    let result = mapArtistNamesToArtistInfo getArtistInfoStub 
                   [| "Nokturanl Mortum"; "Heinali"; "Krobak"|]
    Assert.Equal(result.[0], 1)
    Assert.Equal(result.[1], 3)
    Assert.Equal(result.[2], 2)    

这比典型的可测试的面向对象解决方案要优雅得多,后者需要引入一个接口,将其注入到调用类中,并在测试项目中引入一些重量级的模拟库。
有人可能会争辩说,将一个不纯函数注入到纯函数中 不是真正的函数式方法,但 F# 是一种相当宽容的语言,它允许我们不必提出一些巧妙的概念,比如自由单子等。

错误处理

细心的读者可能已经注意到,我们依赖于网络 API 的无故障工作,这并不是稳健编程的标志。为了正确处理,我们将采用 铁路导向编程的概念。
主要思想是将函数成功和不成功的执行编码到返回类型中,以便管道中的所有函数都能处理成功的结果,并进行一些有用的业务逻辑,而不成功的结果将被排除在进一步执行之外。

但我强烈建议您不要听信我的话,而是阅读原文,它更详细地解释了这个概念。

配方如下

type Result<'TSuccess,'TFailure> = 
    | Success of 'TSuccess
    | Failure of 'TFailure

let switch switchFunction1 switchFunction2 input = 
    match switchFunction1 input with
    | Success s -> switchFunction2 s 
    | Failure f -> Failure f

let (>=>) switchFunction1 switchFunction2 input = 
    switch switchFunction1 switchFunction2 input

现在我们可以将返回值包装到提供的类型中

let getTopArtists () = 
    try
        let path = String.Format(getTopArtistsPattern, baseUrl, userName, apiKey)
        let data = Http.Request(path)
        match data.Body with
        | Text text -> Success(TopArtists.Parse(text).Topartists.Artist)
        | _ -> Failure "getTopArtists. Unexpected format of reponse message"
    with
    | ex -> Failure ex.Message

因此,有了这个,管道将转换为

let pipeline = 
    getTopArtists
        >=> getTopArtistNames
        >=> removeAlreadyRecomendedArtists
        >=> getUrlEncodedArtistNames 
        >=> mapArtistNamesToArtistInfo getArtistInfo
        >=> getArtistsShortInfo
        >=> orderArtistsByListenersCount

让我们也看一下单元测试片段,以了解调用者如何使用函数输出的整体感受

[<Fact>]
    let orderArtistsByListenersCount_returns_expected_result() =
        let Satie = {name = "Erik Satie"; listeners = 750000}
        let Chopin = {name ="Frederic Chopin"; listeners = 1200000}
        let Barber = {name = "Samuel Barber"; listeners = 371000}
        let artists = [|Satie; Chopin; Barber|]
        let result = orderArtistsByListenersCount artists
        match result with
        | Success s -> 
            Assert.Equal(s.[0], Chopin)
            Assert.Equal(s.[1], Satie)
            Assert.Equal(s.[2], Barber)
        | Failure _ -> Assert.True(false)

使用内置的 Result 类型

F# 附带一个内置的 Result 类型,它允许我们放弃 ROPHelper

我们的管道现在看起来如下

let pipeline = 
    getTopArtists()
        |> Result.bind getTopArtistNames
        |> Result.bind removeAlreadyRecomendedArtists
        |> Result.bind getUrlEncodedArtistNames 
        |> Result.bind (mapArtistNamesToArtistInfo getArtistInfo)
        |> Result.bind getArtistsShortInfo
        |> Result.bind orderArtistsByListenersCount

请注意,我们必须使我们的 getTopArtists() 接受 unit 才能被 Result.bind 接受。我们如下模式匹配结果

[<Fact>]
    let getUrlEncodedArtistNames_returns_expected_result() =
        let result = getUrlEncodedArtistNames [|"Bohren & Der Club Of Gore"; "Цукор Біла Смерть"|]
        match result with
        | Ok s ->
            Assert.Equal(s.[0], "Bohren+%26+Der+Club+Of+Gore")
            Assert.Equal(s.[1], 
         "%d0%a6%d1%83%d0%ba%d0%be%d1%80+%d0%91%d1%96%d0%bb%d0%b0+%d0%a1%d0%bc%d0%b5%d1%80%d1%82%d1%8c")
        | Error _ -> Assert.True(false)

结论

我希望今天第一次接触 F# 的人不会觉得这种不常见的语法过于复杂,以至于无法欣赏这种语言的好处,例如类型提供程序或由于函数组合而易于进行单元测试。而且我也希望那些已经站稳脚跟的人觉得铁路导向编程的技术非常有用。

修订历史

  • 2017 年 11 月 7 日 - 初始版本
  • 2019 年 3 月 24 日 - 使用内置的 Result 类型
© . All rights reserved.