Akkaに入門したので、恒例のHello Worldアプリを作ってみました。
なお、Akkaは2.6から「Akka Typed」と呼ばれる、アクター側で受信するメッセージの型を指定する機能が標準となっているので、そちらに準拠しています。1
What's new in Akka 2.6
1. 事前準備
まず、事前準備としてbuild.sbtにAkkaへのDependencyを追加します。
akkaのバージョンは、執筆時点(2020/02/09)で最新版である2.6.3にしました。
またScalaのバージョンは2.13.1を使用することにします。
name := "hello-akka"
version := "0.1"
scalaVersion := "2.13.1"
lazy val akkaVersion = "2.6.3"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
)
なおsbtのバージョンも、同じく執筆時点で最新版である1.3.8にしました。
sbt.version = 1.3.8
2. Hello World!
これでAkkaが使えるようになったので、満を持してHello Worldしてみます。
ソースは以下のようになります
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
// アクターに送信するメッセージ用のクラス。
// StringやIntのようなプリミティブな型を直接送信することもできるが、
// 以下のようにメッセージ用のcase classを作って送信するのが常套手段。
final case class Hello(name: String)
// アクターの振る舞いを定義したオブジェクト。
// 以前はActorトレイトを継承してreceiveメソッドを実装していたが、
// 2.6からはプレーンなオブジェクトになった。
object PrintHello {
// メッセージを受信した際の「振る舞い」を定義する。
// ※Behaviorを返すならなんでもいいはずですが、慣例的にappyメソッドで定義することが多いようです。
def apply(): Behavior[Hello] = Behaviors.receiveMessage { message =>
// 受け取ったメッセージを標準出力。
println(s"Hello ${message.name}!")
// 次にメッセージを受信したときの振る舞いを返す。
// 今回は何度呼ばれても同じ動きをしたいので、Behaviors.sameを使う。
Behaviors.same
}
}
// アクター生成・メッセージ送信処理
object Main extends App {
// 今回作るアクターの「振る舞い」を定義。
// PrintHello()はPrintHello.apply()の略記法。
val behavior: Behavior[Hello] = PrintHello()
// 今回作るアクターの名前。
val name = "PrintHelloMain"
// 指定した「振る舞い」と名前を持つアクターを生成。
val printHelloActor: ActorSystem[Hello] = ActorSystem(behavior, name)
// 送信するメッセージの作成。
val message = Hello("World")
// printHelloActorにメッセージを送信する。
// 「!」メソッド(Bangと読むらしい)はtellメソッドのシンタックスシュガー。
// 以下はprintHelloActor.tell(message)と同じ。
printHelloActor ! message
// printHelloActorはメッセージにHelloクラスのインスタンスしか受け付けないので、
// 以下のように別の型を送信しようとするとコンパイルエラーになる。
// 2.6より前はこれがコンパイルエラーにならなかった(メッセージは全部Any型として処理されてたため)。
/*
printHelloActor ! "Hello World!"
*/
}
以上です。
主な流れとしては、
① アクターに送信するメッセージの型を定義(case class推奨)
② アクターがメッセージを受信した際の振る舞い(処理)を定義(型はBehavior[①の型])
③ アクターを生成(このとき名前と振る舞いを渡す)
④ アクターに送信するメッセージ(①の型のインスタンス)を作成
⑤ ③で作ったアクターに④で作ったメッセージを送信
となります。
上記のプログラムを実行すると、以下のような結果になります。
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Hello World!
最初の3行は、ロガー周りのエラーです。
Akkaは(何も指定しなければ)ロギングにSLF4Jを利用しますが、今回はロガー実装(logbackなど)を指定していないためエラーが出ています。
このエラーは今後も出てきますが、今回の本題ではないため無視(省略)します。
大事なのは4行目です。
printHelloActorにHello("World")というHelloクラスのインスタンスをメッセージとして送信した結果、PrintHello.behavior内で定義された処理が実行されたことがわかります。
また、一度作成したアクターには以下のように複数回メッセージを送信することができます。
object Main extends App {
val behavior: Behavior[Hello] = PrintHello()
val name = "PrintHelloMain"
val printHelloActor: ActorSystem[Hello] = ActorSystem(behavior, name)
// メッセージを送信 × 3
printHelloActor ! Hello("World")
printHelloActor ! Hello("Akka")
printHelloActor ! Hello("Scala")
}
Hello World!
Hello Akka!
Hello Scala!
3. アクターを停止させる
上記のプログラムを実行すると、アクターは望みどおりの動きをするものの、そのままではプログラムが終了しません。
プログラムを終了させるためには、まず生成したアクターを停止させる必要があります。
アクターを停止させるにはBehaviors.stoppedメソッドを使います。
アクターがメッセージを受信した際に上記のメソッドを実行することで、アクターが停止します。
ですが、下記のようにPrintHello#apply内部のBehaviors.sameの部分を、Behaviors.stoppedにそのまま変更してしまうのは少し(かなり)問題があります。
object PrintHello {
def apply(): Behavior[Hello] = Behaviors.receiveMessage { message =>
println(s"Hello ${message.name}!")
// Behaviors.sameをBehaviors.stoppedに変更
// Behaviors.same
Behaviors.stopped
}
}
この状態で、先のプログラムを実行してみます。
object Main extends App {
val printHelloActor: ActorSystem[Hello] =
ActorSystem(PrintHello(), "PrintHelloMain")
// メッセージを送信 × 3
printHelloActor ! Hello("World")
printHelloActor ! Hello("Akka")
printHelloActor ! Hello("Scala")
}
Hello World!
なんと、プログラム自体は終了したものの、最初に送信したメッセージしか処理されていません。
これは、1通目のメッセージを受信し処理した時点でアクターが停止しており、2通目以降のメッセージが受信されていないためです。
これでは複数のメッセージを送信することができません。
解決策としては、以下のように停止命令用のメッセージを作成し、アクターはそのメッセージを受信した場合のみ自身を停止させるようにします。
final case class Hello(name: String)
// 停止命令用メッセージ
case object GoodBye
object PrintHello {
def apply(): Behavior[Hello] =
Behaviors.receiveMessage {
// メッセージがHelloクラスのインスタンスの場合は通常通り
case Hello(name) =>
println(s"Hello ${name}!")
Behaviors.same
// GoodByeオブジェクトを受信した場合は、"Good Bye!"と出力してアクターを停止
case GoodBye =>
println("Good Bye!")
Behaviors.stopped
}
}
object Main extends App {
val printHelloActor: ActorSystem[Hello] =
ActorSystem(PrintHello(), "PrintHelloMain")
// メッセージを送信 × 3
printHelloActor ! Hello("World")
printHelloActor ! Hello("Akka")
printHelloActor ! Hello("Scala")
// HelloクラスではなくGoodByeオブジェクトを送信(停止命令)
printHelloActor ! GoodBye
}
方向性としてはこれで合っているのですが、残念ながら上記のコードはコンパイルが通りません。
これは、printHelloActorはHelloクラスをのみメッセージとして受信できるアクターであり、GoodByeオブジェクトはHelloクラスでもそのサブクラスでもないからです。
では、複数の型をメッセージとして同じアクターに送信することはできないのでしょうか?
それは部分的にイエスで、部分的にノーです。
一つのアクターが一つの型のメッセージしか受け取らないというのなら、HelloもGoodByeも、同じ型のサブクラスにしてしまえば良いのです。
ここで、メッセージの型を抽象化するHelloCommandトレイトを用意し、HelloクラスとGoodByeオブジェクトをそのサブクラスに変更します。
// 送信メッセージの型を抽象化するトレイト
sealed trait HelloCommand
// HelloとGoodByeはHelloCommandのサブクラスにする
final case class Hello(name: String) extends HelloCommand
case object GoodBye extends HelloCommand
更に、アクターが受信するメッセージの型を、HelloクラスからHelloCommandトレイトに変更します。
これで、アクターはHelloCommandのサブクラスなら何でもメッセージとして受け取れるようになります。
あとはメッセージの型ごとにパターンマッチで処理を分岐させるだけです。
object PrintHello {
// Behavior[Hello]からBehavior[HelloCommand]にすることで
// HelloクラスもGoodByオブジェクトもメッセージとして受信できるようになる
def apply(): Behavior[HelloCommand] =
Behaviors.receiveMessage {
// Helloクラスを受信した場合は画面に出力
case Hello(name) =>
println(s"Hello ${name}!")
Behaviors.same
// GoodByeオブジェクトを受信した場合は、"Good Bye!"と出力してアクターを停止
case GoodBye =>
println("Good Bye!")
Behaviors.stopped
}
}
これで、PrintHelloアクターはHelloとGoodByeのどちらの型もメッセージとして受信できるようになりました。
以下に完全な例を示します。
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
sealed trait HelloCommand
final case class Hello(name: String) extends HelloCommand
case object GoodBye extends HelloCommand
object PrintHello {
def apply(): Behavior[HelloCommand] =
Behaviors.receiveMessage {
case Hello(name) =>
println(s"Hello ${name}!")
Behaviors.same
case GoodBye =>
println("Good Bye!")
Behaviors.stopped
}
}
object Main extends App {
val printHelloActor: ActorSystem[HelloCommand] =
ActorSystem(PrintHello(), "PrintHelloMain")
// 送信したメッセージの標準出力 × 3
printHelloActor ! Hello("World")
printHelloActor ! Hello("Akka")
printHelloActor ! Hello("Scala")
// アクターを停止させる
printHelloActor ! GoodBye
// 既にアクターは停止しているため、下記のメッセージは標準出力されないはず
printHelloActor ! Hello("Not Print")
}
上記のプログラムの実行結果は以下のようになります。
Hello World!
Hello Akka!
Hello Scala!
Good Bye!
想定通り、アクターが停止し、出力結果だけではわかりづらいですがプログラム自体も終了しました。
また、アクター停止後に送信したメッセージは処理されていないことがわかります。
最後に
今回は自身の学習記録として、アクターの生成とメッセージの送信、アクターの停止に挑戦してみました。
Lightbendが公式で出しているクイックスタートガイドより更に初歩的な内容のため、実用性は乏しいですが、同じくこれからAkkaを学習する方の参考になれば幸いです。
最後までお読み頂きありがとうございました。
質問や不備についてはコメント欄かTwitterまでお願いします。
-
「Akka Typed」モジュール自体は以前からあったのですが、2.6までは"May Change"モジュールとして、いわば実験版のような扱いでした。それまではアクター側はどんな型のメッセージも受信できたため、想定していない型を受信した場合は実行時エラーになる可能性がありました。 ↩