此代码来自 akka 文档。它使用推荐的函数式风格实现一个 Actor :
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 堆栈足够大)。
是否可以使调用尾部递归,或者我是否必须回退到具有可变变量的面向对象风格来处理这些情况?
最佳答案
简短的回答是它不是递归的,因为 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
的调用包含在 CounterFunction
的 apply
方法中,因此它们不会发生,直到 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 :
a loop that iteratively invokes [functions which return unevaluated function objects].... Programmers can use trampolined functions to implement tail-recursive function calls in stack-oriented programming languages.
在这种特殊情况下,“未评估的函数对象”是 Behavior
示例实现中的处理器
。
关于scala - 如何在 Behaviors.receive 中进行递归调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64972768/