Scala Tail递归未来的回归

时间:2018-03-10 02:26:24

标签: scala future tail-recursion

如何在scala中实现尾递归函数,将future作为返回值:

示例代码

 def getInfo(lists: List[Int]): Future[List[Int]] = {
  def getStudentIDs(lists: List[Int]): Future[List[Int]] = {
    //here a web service call that returns a future WS response
        val response=ws.call(someURL)

        response.map(r => {
          r.status match {

            case 200 =>
              var newList = lists + (r.json \ ids)
                .as[List[Int]] //here add the ws call response json..
              getStudentIDs(newList)
            case 429 =>Future.sucessful(lists)
            case _ => getStudentIDs(lists)
          }

        })
      }
      getStudentIDs(List.empty[Int])
    }

2 个答案:

答案 0 :(得分:0)

这是问题(简化了使其可运行的代码):

import scala.concurrent._
import scala.concurrent.duration._
import annotation.tailrec   
import scala.concurrent.ExecutionContext.Implicits.global

 def getInfo(lists: List[Int]): Future[List[Int]] = {
  @tailrec
  def getStudentIDs(lists: List[Int]): Future[List[Int]] = {
     Future(List(1, 2, 3)).flatMap(x => getStudentIDs(x ::: lists))
  }
     getStudentIDs(List.empty[Int])
  }

给出错误:

error: could not optimize @tailrec annotated method getStudentIDs: 
it contains a recursive call not in tail position
            Future(1).flatMap(x => getStudentIDs(x :: lists))
                      ^

问题不仅在于Future。实际问题是getStudents不在终端/尾部位置 - 它是从map调用的。如果您不使用Futures并使用常规map来自集合或任何其他函数,那么这将是一个问题。例如:

 def getInfo(lists: List[Int]): List[Int] = {
  @tailrec
  def getStudentIDs(lists: List[Int]): List[Int] = {
     List(1).flatMap(x => getStudentIDs(x :: lists))
  }
     getStudentIDs(List.empty[Int])
  }

给出了同样的错误:

error: could not optimize @tailrec annotated method getStudentIDs:
it contains a recursive call not in tail position
            List(1).flatMap(x => getStudentIDs(x :: lists))
                    ^

这让您感到困难的是,您可以直接从未来获得结果,以便在getStudents中使用它,因为您不知道它是否已经完成并且#&#39} 39;阻止未来并等待结果不是一个好习惯。所以你有点被迫使用map。这是一个非常糟糕的例子,说明如何使尾递归(仅适用于科学:))。不要在生产代码中这样做:

 def getInfo(lists: List[Int]): Future[List[Int]] = {
  @tailrec
  def getStudentIDs(lists: List[Int]): Future[List[Int]] = {
     val r = Await.ready(Future(List(1, 2, 3)), Duration.Inf).value.get.getOrElse(lists)
     getStudentIDs(r ::: lists)
  }
     getStudentIDs(List.empty[Int])
  }

编译器很高兴,但这可能会导致许多问题 - 请阅读Awaitblocking和线程池以获取更多信息。

我认为你的函数不是尾递归可能不是一个大问题,因为你可能不想以这种方式创造很多未来。如果它真的像演员(Akka)等问题那么你可以尝试其他并发框架。

答案 1 :(得分:0)

我认为这是一个XY问题。你可能不想要它"尾递归"在"具有@tailrec" -annotation的意义上。你想要的是堆栈安全性,这样这种方法在几百次重试之后就不会将堆栈连接到你的web服务。

为此,有一些库,例如Cats

在Cats中,有一个名为Monad的类型类,这个类型类为你想要的东西提供了一种特殊的方法:

 tailRecM[A, B](a: A)(f: (A) => F[Either[A, B]]): F[B] 

来自文档的引用:

  

保持调用f直到返回scala.util.Right [B]。   此方法的实现应使用常量堆栈空间[...]

Future in FutureInstances可以实现此功能。但实现似乎微不足道,因为它混合在StackSafeMonad

您当然可以查看StackSafeMonad的实现,然后尝试理解为什么它在Future的情况下就足够了,但您也可以只使用库实现而不用担心您的递归方法是否会因StackOverflowError而失败。