玩 Scala:不使用 Await.result 的递归 Web 服务调用

Play Scala : Recursive web services call without using Await.result

我在 Play! Scala 2.2 工作,我需要进行递归 Web 服务调用(直到我获得所有结果)。 目前我设法用 Await.result 做到了,如下所示:

def getTitleSetFromJson( jsValue: JsValue ): Set[ String ] = {
  val titleReads: Reads[Option[String]] = (__ \ "title").readNullable[String]
  (jsValue \ "response" \ "songs")
    .asOpt[Set[Option[String]]](Reads.set(titleReads))
    .getOrElse(Set.empty)
    .flatten    
}

def findEchonestSongs(start: Long, echonestId: String): Future[Set[String]] = {
  val titleReads: Reads[Option[String]] = (__ \ "title").readNullable[String]
  val endpoint = s"$baseUrl/artist/songs"
  val assembledUrl = s"$endpoint?api_key=$echonestApiKey&id=$artistId&format=json&start=$start&results=20"

  WS.url(assembledUrl).get().map { songs =>
    if ((songs.json \ "response" \ "total").asOpt[Int].getOrElse(0) > start + 20) {
      getTitleSetFromJson(songs.json) ++
        Await.result( findEchonestSongs(start + 20, echonestId), 3.seconds )
    } else {
      getTitleSetFromJson(songs.json) 
    }
  }
}

现在我想做同样的事情,但使用非阻塞方式,即不使用 Await.result 但是我尝试的所有方法都出现错误:由于嵌套的期货,类型不匹配。

你能告诉我这个方法吗?

您可以将其他(来自嵌套调用)未来的结果映射到当前集合中。

还有...一件很重要的事。 Readability Counts... 特别是在 Whosebug 上。

def findEchonestSongs(start: Long, echonestId: String): Future[Set[String]] = {

  val titleReads: Reads[Option[String]] = (__ \ "title").readNullable[String]

  val requestUrl = "http://developer.echonest.com/api/v4/artist/songs?api_key=" + echonestApiKey + "&id=" + echonestId +
  "&format=json&start=" + start + "&results=20"

  WS.url( requestUrl ).get().flatMap { songs =>

    var nTotal = (songs.json \ "response" \ "total").asOpt[Int].getOrElse(0)

    if( nTotal > start + 20 ) {

      val titleSet = getTitleSetFromJson( songs.json )

      val moreTitleSetFuture = findEchonestSongs( start + 20, echonestId )

      moreTitleSetFuture.map( moreTitleSet =>  titleSet ++ moreTitleSet )

    }  
    else {
      Future.successful( getTitleSetFromJson( songs.json ) )
    }
  }
}

def getTitleSetFromJson( jsValue: JsValue ): Set[ String ] = {
   (songs.json \ "response" \ "songs")
    .asOpt[Set[Option[String]]](Reads.set(titleReads))
    .getOrElse(Set.empty)
    .flatten
}

有许多不同的方法可以实现您的要求。但也许最简单的方法是使用 flatMap.

这是你的函数的改进版本(为了让它更简单,我冒昧地把它改成了 return Future[Set[JsValue]])

import play.api.libs.json.JsValue
import play.api.libs.ws.WS
import scala.concurrent.Future
import play.api.Play.current
import scala.concurrent.ExecutionContext.Implicits.global

object Test {

  val apiKey = "//YOUR_ECHONEST_API_KEY//"
  val baseUrl = "http://developer.echonest.com/api/v4"

  def findSongs(start: Long, artistId: String): Future[Set[JsValue]] = {

    def endpoint = s"$baseUrl/artist/songs"
    def assembledUrl = s"$endpoint?api_key=$apiKey&id=$artistId&format=json&start=$start&results=20"
    def response = WS.url(assembledUrl).get()
    def futureJson = response map (_.json)
    futureJson flatMap { result =>
      def total = (result \ "response" \ "total").asOpt[Int]
      def songs = (result \ "response" \ "songs").as[Set[JsValue]]
      total exists (_ > start + 20) match {
        case false => Future.successful(songs)
        case true => findSongs(start + 20, artistId) map (songs ++ _)
      }
    }

  }

}

处理结果

如果您真的想要 Set[String],您可以轻松地对结果使用 map

val sample = findSongs(0, "someone")
sample.map(_.map(_ \ "title").map(_.as[String]))

改进

这里有很大的改进空间。例如,每次递归调用函数时都不需要传递 artistId 。所以我们可以定义一个递归的内部函数。

使用累加器使其成为尾递归并不难。

最后,使用 Seq[Future]fold 代替递归可能是有意义的,递归也是函数式编程中的常见模式。但是 Stream api 已经很好地提供了所有这些东西。您可以参考 Stream API 了解更多信息。