とらきす の ぐだログ

とらきすのぐだログ

ぐだぐだとなんかするブログ。

Akka を触ってみる 2 - Akka Actor 編 その2

前回は こちら


今回は、子アクターの生成について軽く見ていこう。要件は、こう:

  • 親となる ParentActor、子となる ChildActor を用意する。
  • ParentActor は、生成と同時に ChildActor を子アクターとして生成し、そこへ Request を送信する。Response を受信したら、自らを停止する。
  • ChildActor は、Request を受信したらその中にある文字列を標準出力し、送信元に Response を送信する。

コードはこちら:

Request.scala
import akka.actor.typed.ActorRef

case class Request(message: String, reptyTo: ActorRef[Response])
Response.scala
case class Response()
ParentActor.scala
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object ParentActor {
  def apply(): Behavior[Response] =
    Behaviors.setup { context =>
      val child = context.spawn(ChildActor(), "print-worker")
      child ! Request("Hello, World!", context.self)

      Behaviors.receiveMessage { _ =>
        println("received response.")

        Behaviors.stopped
      }
    }
}
ChildActor.scala
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object ChildActor {
  def apply(): Behavior[Request] =
    Behaviors.receiveMessage { request =>
      println(request.message)
      request.replyTo ! Response()

      Behaviors.same
    }
}
Main.scala
import akka.actor.typed.ActorSystem

object Main extends App {
  val system = ActorSystem(ParentActor(), "parent-actor")
}


ちょっと前回よりも長くなっているが、最も注意を向けるべきは ParentActor だ:

ParentActor.scala
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object ParentActor {
  def apply(): Behavior[Response] =
    Behaviors.setup { context =>
      val child = context.spawn(ChildActor(), "print-worker")
      child ! Request("Hello, World!", context.self)

      Behaviors.receiveMessage { _ =>
        println("Response received.")

        Behaviors.stopped
      }
    }
}

前回は Behaviors.receiveMessage(...) しかなかったのが、それをさらに Behaviors.setup(...) で包んでいる。
Behaviors.setup(...) は、アクターを生成してからメッセージを受け取るまでの間にやっておきたい 前処理 を書いておけるメソッドである。ここで指定した処理は、(基本的に) アクター生成時の一度だけ実行される。

...わざわざ “基本的に” と強調して書いたのは、ここに状態遷移が組み合わさってくると少し事情が異なってくるからだ。といっても、よく考えれば当たり前のことではあるし、対処も非常に簡単ではあるのだが... これは次回説明する。

Behaviors.setup(context => ???)Behaviors.receiveMessage(message => ???) をまとめてしまいたいときは、Behaviors.receive((context, message) => ???) なんてメソッドも用意されている。これが必要になるような場面はあまり思い浮かばないが。

Behaviors.setup(...) の中では、そのインスタンス固有の操作をすることができる ActorContext が使えるようになるので、いろいろうれしい。 まず、ActorContext.spawn(...) で子アクターを生成できる。このメソッドは子アクターを生成したあと ActorRef (アクターインスタンスへの参照) を返すので、変数に入れておくと便利。
名前を指定するのが面倒なら、ActorContext.spawnAnonymous(...) を使えばランダムな名前を割り当ててくれる。
なお、Classic Actor では ActorRef.actorOf(...) で子アクターを生成できていたが、Typed Actor では廃止されている。

ほかにも、ActorContext.self で自分の ActorRef を取得したり、ActorContext.system で自分が所属する ActorSystem の ActorRef を取得したり、子アクターを取得したり停止させたり... いわゆる “ask” メソッドも用意されている (などと格好つけているが、正直 Ask パターンのことはなにも知らない。また後日調べて書く)。

そしてメッセージを受信したら、(前回のように Behaviors.same ではなく) Behaviors.stopped を返すようにしている。これは名のごとく、このアクターを停止させることを表している。このとき、子アクターである print-worker も一緒に停止する。


また、Request.scala の中身を見てみるとわかるが:

Request.scala
import akka.actor.typed.ActorRef

case class Request(message: String, replyTo: ActorRef[Response])

返信先の ActorRef を添付するようにしている。Classic Actor ではこんなことをせずとも sender というオブジェクトが暗黙的に定義されていたが、Typed Actor ではこのように明示的に作ってやる必要がある。
なぜ sender ではなく replyTo という名称にしてあるのかと言えば、まぁ Akka Actor のドキュメントがそうしていたからというのもあるが、もし返信先を送信元と別のところに変えたいケースが出てきたときに sender という名称では不自然だからという理由がある。


今回はここまで。ちょっと詰めが甘いかもですね。まぁいいか。