玩 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 了解更多信息。
我在 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 了解更多信息。