ChatWork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

Akka Typed 触ってみた感想

これは ChatWork Advent Calendar 2017 - Adventar の、14日目の記事です。
13日目は id:cw-nishiguchi による チャットワークのクラス設計を見直した話 - ChatWork Creator's Note でした。


こんにちは。プロダクト開発部の@hayasshi_です。
Scala のプロダクトを開発しています。

弊社の Scala プロダクトは主に Akka をもちいて作られています。
Akka Actor は、並行分散処理を実現するツールキットとして非常に強力ですが、メッセージングのインターフェース部分が型安全ではないという問題があります。

せっかく Scala で書いているのですから、なんでも静的に型検査したいですよね。
そこで今回は、Akka Actor のメッセージングを型安全におこなうための、Akka Typed というモジュールを触ってみたいと思います。
(まだ may change なモジュールのためプロダクト投入はしていません)

触ってみた感想

最初に結論といいますか、触ってみたところの感想です。

振る舞い(Behavior)を記述するだけでアクターを隠蔽しながら型安全にメッセージングをおこなえました。
アクターのライフサイクルも、Signal というかたちで振る舞いに付加できました。

ただ、プログラミングパターンが完全に別物で頭の切り替えが必要なことと、既存の他モジュール(Akka Remote や Akka Streams など)との連携や、Supervisor 等でのアクターの監督パターンをどのように書けばいいのか、プラクティス的なものを今後探っていく必要があると感じました。

まだ may change なので、引き続きウォッチを続けていきたいと思います。
(われながら当たり障りのない結論になってしまいました...)

注意!

Akka Typed は現時点では may change です。
APIインターフェースの breaking change が行われたり、バイナリ互換が保証されない変更が行われる可能性があります。
(以前はMavenのアーティファクトIDのサフィックスに、-experimentalとつけられていましたが、モジュールにmay changeとマークする方式に変更された模様です。)

Migration Guide 2.4.x to 2.5.x • Akka Documentation

検証で触ったバージョンは、Scala = 2.12.3, Akka = 2.5.8になります。

Akka Actor

まず、Akka Actor のおさらいです。 Akka Actor は、メニーコア時代のマシンリソースを最大限利用する並行分散アプリケーションを、安全に、そして簡潔に実装するためのツールキットです。
Erlang などと同様、アクターモデル を採用し、Scala / Java でアクタープログラミングを行い、アプリケーションをつくれます。

詳しい説明はここでは行いませんが、Akkaについて詳しく書かれた Akka in Action と、その邦訳版である Akka実践バイブル アクターモデルによる並行・分散システムの実現 が最近発売されたようですので、Akka を深く知りたい方はこちらを参照されると良いと思われます。

Akka Actor のメッセージングインターフェース

簡単な例として、Counter を実装してみます。

class Counter extends Actor {
  import Counter._

  var count = 0

  override def receive: Receive = {
    case cmd: Command =>
      // match式にかけ網羅チェックをおこなう
      cmd match {
        case Increment(value) =>
          count += value
        case GetCount =>
          sender() ! Count(count)
      }
  }
}

object Counter {
  sealed trait Command
  case class Increment(value: Int = 1) extends Command
  case object GetCount extends Command

  case class Count(value: Int)

  def props(): Props = Props(classOf[Counter])
}
import akka.actor._
import akka.pattern.ask
import Counter._

val system = ActorSystem("counter")
val counter = system.actorOf(Counter.props())

counter ! Increment()
counter ! Increment(3)
counter ! Count(3) // unhandled

import scala.concurrent.duration._
import system.dispatcher
implicit val timeout = akka.util.Timeout(1.second)
(counter ? GetCount).mapTo[Count].foreach(println(_)) // Count(4)

Await.ready(system.terminate(), 5.second)

Akka Actor の !(tell メソッド)の引数の型はAnyになっており、自分で定義した Actor の receive メソッドの想定メッセージと異なる型を受け取っても、コンパイル時に発見することができません。
Actor の recive メソッドにマッチしないメッセージはunhandledとして捨てられてしまい、実行時例外として検出もできないので不具合のもとになってしまいます。

またakka.actor.Actor.Receiveは、type Receive = PartialFunction[Any, Unit]になっており、Askパターンでの戻り値もFuture[Any]になるので、mapTo メソッドで適切な型に変換する必要があります。(mapTo に想定外の型が設定されると、実行時にClassCastExceptionで失敗した Future になります。)

このように Akka Actor のメッセージング部分は頭のなかでしっかり型を意識し、注意して実装する必要があります。

Akka Typed

Akka Actor ではアクター自体を定義していく形でしたが、Akka Typed これまでと異なり、振る舞い(Behavior)を定義する形で記述します。

object TypedCounter {
  sealed trait Command
  case class Increment(value: Int = 1) extends Command
  case class GetCount(replyTo: ActorRef[Count]) extends Command

  case class Count(value: Int)

  val counter: Behavior[Command] = nextCounter(0)

  private def nextCounter(count: Int): Behavior[Command] =
    Actor.immutable[Command] { (_, cmd) =>
      cmd match {
        case Increment(value) =>
          nextCounter(count + value)
        case GetCount(replyTo) =>
          replyTo ! Count(count)
          Actor.same
      }
    }
}
import TypedCounter._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

val system = ActorSystem(counter, "typed-counter")

implicit val timeout = akka.util.Timeout(1.second)
implicit val scheduler = system.scheduler

system ! Increment()
system ! Increment(3)
// system ! Count(3) // compile error!

val f: Future[Count] = system ? (GetCount(_))
f.foreach(println) // Count(4)

Await.ready(system.terminate(), 5.seconds)

Behaviorの型パラメータで指定されたもの以外の型が!(tell メソッド)で指定されていた場合、コンパイル時に検出できるようになりました。
これは、ActorRef[Command]という型で ActorRef が受け取れるメッセージの型を型パラメータとして指定できるようになったためです。

Askパターンにおいても、mapTo をつかわなくても期待する型の Future が取得できるようになりました。
(Akka Typed では、不変な振る舞いを記述するため、sender()が利用できなくなっており、?(ask メソッド)の引数にActorRef[Count] => TypedCounter.CountのようなBehaviorに応じた型パラメータを持つ関数を受け取り、リプライ先をメッセージに含めることで実現できるようになっています。)

状態を持たせる必要がある場合は、新しい状態を持つBehaviorを作り出し、次の振る舞いにすることで実現しています。

今までの Akka Actor での書き方と大きく異なるものの、型安全にメッセージングができていることがわかります。

しかし、最初の所感で書いた通り、Akka Actor とはパッケージも書き方も大きく異なるため、既存のモジュールとの連携はどのように書いたらいいかであったり、Akka Typed でのコーディングプラクティスなどに関しては、引き続きドキュメントやコードを読んで探っていく必要があると感じました。

まとめ

  • Akka Typed で型安全にアクターのメッセージングをおこなうことができる
  • Akka Typed では振る舞いを記述していく
  • まだまだ触り足りないので、ドキュメントやコードを読んでプラクティスを探っていく段階であると感じた

参考情報

この記事を社内レビューに出したところ、強い方々が Akka Typed に関わる参考情報を教えてくれました。