Scala
Akka
actor

Akka actor で状態の var を機械的に書き換えて無くす方法

More than 1 year has passed since last update.

Actor では状態を保持するために var を使うことがあります。Actor の receive は並列に実行されることはないため、var でも並列性にまつわる問題に悩まされることはありません。
しかし同一スレッドでも再代入無しのほうが何かと問題が起きづらいですよね。

Actor で状態を表す var は機械的に書き換えて無くすことが出来ます。
次のようなコードがあったときに、

class MyActor extends Actor {
  private var myState: MyState = 初期状態

  override def receive: Receive = {
    case ... =>
      // 状態を参照して何か処理
      myState = 更新された状態 // 状態更新
  }
}

こうする

class MyActor extends Actor {
  override def receive: Receive = stateToReceive(初期状態)

  private def stateToReceive(myState: MyState): Receive = {
    case ... =>
      // 状態を参照して何か処理
      context.become(stateToReceive(更新された状態))
  }
}

はい。var 無くなりました。

activator new で minimal-akka-scala-seed テンプレートで生成された Actor を書き換えてみましょう。

元のコードがこれ

class PingActor extends Actor with ActorLogging {
  import PingActor._

  var counter = 0
  val pongActor = context.actorOf(PongActor.props, "pongActor")

  def receive = {
    case Initialize => 
      log.info("In PingActor - starting ping-pong")
      pongActor ! PingMessage("ping")   
    case PongActor.PongMessage(text) =>
      log.info("In PingActor - received message: {}", text)
      counter += 1
      if (counter == 3) context.system.shutdown()
      else sender() ! PingMessage("ping")
  } 
}

var counter が今回のターゲットです。書き換えて var をなくしてみましょう。

class PingActor extends Actor with ActorLogging {
  import PingActor._

  def receive = stateToReceive(0)
  val pongActor = context.actorOf(PongActor.props, "pongActor")

  private def stateToReceive(counter: Int): Receive = {
    case Initialize =>
      log.info("In PingActor - starting ping-pong")
      pongActor ! PingMessage("ping")
    case PongActor.PongMessage(text) =>
      log.info("In PingActor - received message: {}", text)
      context.become(stateToReceive(counter + 1))
      if (counter + 1 == 3) context.system.shutdown()
      else sender() ! PingMessage("ping")
  }
}

はい。できました。
差分はこんな感じです。

--- a/src/main/scala/com/example/PingActor.scala
+++ b/src/main/scala/com/example/PingActor.scala
@@ -5,17 +5,18 @@ import akka.actor.{Actor, ActorLogging, Props}
 class PingActor extends Actor with ActorLogging {
   import PingActor._

-  var counter = 0
+  def receive = stateToReceive(0)
   val pongActor = context.actorOf(PongActor.props, "pongActor")

-  def receive = {
+  private def stateToReceive(counter: Int): Receive = {
     case Initialize =>
       log.info("In PingActor - starting ping-pong")
       pongActor ! PingMessage("ping")
     case PongActor.PongMessage(text) =>
       log.info("In PingActor - received message: {}", text)
-      counter += 1
-      if (counter == 3) context.system.shutdown()
+      context.become(stateToReceive(counter + 1))
+      if (counter + 1 == 3) context.system.shutdown()
       else sender() ! PingMessage("ping")
   }
 }

注意することは、更新後の counter を参照していた箇所(counter == 3)で、更新が行われないので counter + 1 == 3 と書き換える点くらいでしょうか。

複数の状態変数 var があるときは、stateToReceive() の引数を複数個にするなどすれば大丈夫です。