【问题标题】:How to make recursive calls within Behaviors.receive?如何在 Behaviors.receive 中进行递归调用?
【发布时间】:2020-11-23 16:55:58
【问题描述】:

此代码来自 akka 文档。它使用推荐的函数式风格来影响演员:

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors

object Counter {
  sealed trait Command
  case object Increment extends Command
  final case class GetValue(replyTo: ActorRef[Value]) extends Command
  final case class Value(n: Int)

  def apply(): Behavior[Command] =
    counter(0)

  private def counter(n: Int): Behavior[Command] =
    Behaviors.receive { (context, message) =>
      message match {
        case Increment =>
          val newValue = n + 1
          context.log.debug("Incremented counter to [{}]", newValue)
          counter(newValue)
        case GetValue(replyTo) =>
          replyTo ! Value(n)
          Behaviors.same
      }
    }
}

actor 包含一个递归调用“counter(newValue)”以通过函数方式维护可变状态。当我实现它并将@tailrec 注释添加到函数时,scala 编译器会抱怨调用不是尾递归,即使它似乎在最后一个位置。这意味着,迟早会发生堆栈溢出异常(假设您只想计算所有传入的消息并且有数十亿条消息 - 没有足够大的 Java 堆栈)。

是否可以使调用尾递归,或者我必须回退到具有可变变量的面向对象样式来处理这些情况?

【问题讨论】:

  • 它不是尾递归,因为函数所做的最后一件事是创建行为。别担心。这是使用 Akka Typed 的推荐方式。
  • 这能回答你的问题吗? Akka context become recursive function

标签: scala recursion akka akka-typed


【解决方案1】:

简短的回答是它不是递归的,因为counter 所做的最终归结为:

  • 它创建一个Function2[ActorContext[Command], Command, Behavior[Command]] 的实例
  • 它将该实例传递给Behaviors.receive,后者使用它构造一个Behaviors.Receive[Command] 对象(扩展Behavior[Command]

详细说明:

虽然这不是任何最近的 Scala 编译器执行的确切转换,但这应该让您了解为什么它不是递归的

object Counter {
  // Protocol omitted
  class CounterFunction(n: Int) extends Function2[ActorContext[Command], Command, Behavior[Command]] {
    override def apply(context: ActorContext[Command], message: Command): Behavior[Command] =
      message match {
        case Increment =>
          // omitting logging, etc.
          counter(n + 1)
        case GetValue(replyTo) =>
          replyTo ! Value(n)
          Behaviors.same
      }
  }

  private def counter(n: Int): Behavior[Command] = {
    val f = new CounterFunction(n)
    Behaviors.receive(f)
  }
}

请注意,由于对counter 的调用包含在CounterFunctionapply 方法中,因此它们不会在调用apply 之前发生,直到实际处理消息时才会发生。

这不会溢出堆栈,从这个与 Akka 内部深处的实现没有太大区别的东西的最小实现中可以看出:

case class Behavior[T](
  processor: (ActorContext[T], T) => Behavior[T]
)

object Behavior {
  def processMsgs[T](b: Behavior[T], ctx: ActorContext[T])(msgs: List[T]): Behavior[T] =
    // No recursion here...
    msgs.foldLeft(b) { (behavior, m) => behavior.processor(ctx, m) }
}

Behavior.processMsgs 函数是已知的一个示例(尤其是在函数式编程语言实现社区中),如 trampoline

一个迭代调用[返回未计算函数对象的函数]的循环......程序员可以使用蹦床函数在面向堆栈的编程语言中实现尾递归函数调用。

在这种特殊情况下,“未评估的函数对象”是 Behavior 的此示例实现中的 processor

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多