Scala Tail 递归未来 returns

Scala Tail recursion future returns

如何在 Scala 中使用 futures 作为 return 值实现尾递归函数:

示例代码

 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])
    }

这是问题所在(代码经过简化以使其可运行):

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 不在 terminal/tail 位置 - 它是从 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))
                    ^

这里的困难在于,您不能直接从 future 中获取结果以在 getStudents 中使用它,因为您不知道它是否已完成,并且阻止它不是一个好习惯未来,等待结果。所以你有点被迫使用 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 和线程池以了解更多信息。

我认为您的函数不是尾递归可能不是什么大问题,因为您可能无论如何都不想以这种方式创建很多期货。如果这真的是一个问题,比如 actors (Akka) 等,您可以尝试其他并发框架

我认为是 XY-problem。在 "having a @tailrec" 注释的意义上,您可能不想要它 "tail-recursive"。你想要的是堆栈安全,这样在重试数百次连接到你的网络服务后,此方法不会破坏堆栈。

为此,有库,例如 Cats

在 Cats 中,有一个名为 Monad 的类型类,这个类型类提供了一个特殊的方法,似乎正是您想要的:

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

引自文档:

Keeps calling f until a scala.util.Right[B] is returned. Implementations of this method should use constant stack space [...]

Future in FutureInstances available. The implementation seems trivial though, because it mixes in StackSafeMonad.

有一个实现

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

而失败