This code is from akka documentation. It impelements an actor using the recommended functional style:
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
}
}
}
The actor contains a recursive call "counter(newValue)" to maintain mutable state by functional means. When I implement this and add the @tailrec annotation to the function, the scala compiler complains as the call is not tail recursive, even it seems to be in the last position. This means, sooner or later a stack overflow exception will occur (imagine you just want to count all incoming messages and there are some billions of them - no java stack would be big enough).
Is it possible to make the call tail recursive or do I have to fallback to the object oriented style with mutable variables to handle those cases?
The short answer is that it's not recursive, because what
counter
is doing, ultimately, boils down to:Function2[ActorContext[Command], Command, Behavior[Command]]
Behaviors.receive
, which uses it to construct aBehaviors.Receive[Command]
object (which extendsBehavior[Command]
)To elaborate:
While this isn't the exact transformation performed by any recent Scala compiler, this should give you the flavor of why it's not recursive
Note that since the call to
counter
is wrapped insideCounterFunction
'sapply
method, they don't happen until thatapply
is called, which isn't until a message is actually being processed.This will not overflow the stack, as can be seen with this minimal implementation of something that's not that different from the implementation deep within Akka's internals:
The
Behavior.processMsgs
function is an example of what's known (especially in the functional programming language implementation community), as a trampoline:In this particular case, the "unevaluated function object" is the
processor
in this sample implementation ofBehavior
.