Akka Typed 移行入門 (Scala)

はじめに

こんにちは、オープンエイトの山崎です。

今回は、いわゆる Actor モデルScala/Java 実装である Akka に関して、v2.6.0 から導入された Akka Typed の簡単な紹介と既存の Actor (Classic Actor) から Typed Actor への移行方法について Scala 言語を用いて説明します。記事執筆時の環境は以下の通りです。

  • Scala 2.13.4
  • Akka 2.6.11

オープンエイトでは、SNS 配信効果測定サービス Insight BRAIN において Akka Actor、Akka Http、Akka Streams と全面的に Akka を採用してシステムを構築していますが、その中で昨年部分的に Akka Typed を導入しました。

Akka Typed は既存の Classic Actor の課題を解決しコードをよりシンプルかつ堅牢なものにしてくれるなど、導入することで得られるメリットは決して小さくないのですが、書籍「Akka 実践バイブル (Akka in Action)」でもほんの少ししか触れられていないなどまだ日本語の情報が十分ではない状況です。

簡単な内容ではありますが、本記事が Akka Typed 導入の参考になれば幸いです。

Akka Typed について

Akka Typed は Akka 2.6.0 から導入された新しい Actor の API です。それまでの Actor API は Classic Actor と呼ばれ区別されています。Akka の公式ドキュメントではすでに Akka Typed での説明がメインになっています。

Akka Documentation
https://doc.akka.io/docs/akka/current/

Akka Typed を導入するメリットとしては以下のような点が挙げられます。

  • メッセージの型を宣言できる
  • Immutable なコードで Actor を実装できる

メッセージの型を宣言できる

Typed Actor では Actor が受信するメッセージの型を明示できます。

下記サンプルコードでは Message 型のメッセージを受信する Actor を定義しています。

import akka.actor.typed.scaladsl.{ Behaviors }
import akka.actor.typed.{ ActorRef, ActorSystem, Behavior }

trait Message
case object Print extends Message
case class Update(numValue: Int) extends Message

object MyTypedActor {
  def apply(numValue: Int): Behavior[Message] =
    Behaviors.receive[Message] { (ctx, msg) =>
      msg match {
        case Print =>
          println(numValue)
          Behaviors.same
        case Update(numValue) =>
          MyTypedActor(numValue)
      }
    }
}

Actor の記述方法が Classic Actor とはだいぶ違っているので最初は戸惑いますが、やっていることは Messagereceive して何らかの処理をするだけなのでその点においては Classic Actor と変わりません。

Classic Actor で少し残念な部分の一つはメッセージの型を宣言できないことではないかと思います。パターンマッチでフィルタリングすればいいのですが、実行時に動的に型判定することになります。せっかく Scala で書いているのだから Actor も Type Safe に静的に書きたくなるものです。Typed Actor はその名の通りまさにその課題を解決してくれます。

メッセージを送信するクライアントコードは以下のようになります。

val system: ActorSystem[Message] =
  ActorSystem(MyTypedActor(123), "my-actor")

system ! Print
system ! Update(456)
system ! Print
system ! Update(789)
system ! Print
system ! "hello" // コンパイルエラー

異なる型のデータを送信しようとするとコンパイルエラーになります。コンパイル時に型チェックができるのでコーディングミスによる余計なランタイムエラーの削減につながります。

Actor 側でも、受け取る型が決まっているので case _ => のようなフォールバックコードが不要になります。また、網羅性のチェックも Scala のパターンマッチの仕組みを利用してコンパイル時に行うことができます。そのように全体的にコードがすっきりして見通しがよくなりかつ安全性も高まります。

Immutable なコードで Actor を実装できる

Actor モデルはその概念上ステートフルであるため、Actor は基本的に Mutable なインスタンスになります。そのため Actor に何かしら状態を管理させたい場合は Mutable なプロパティを持たせる必要があります。これはそういうものなので、まあ、そういうものなのですという感じなのですが、Akka Typed では関数型的な仕組みを提供していて、関数の再帰処理で Actor を記述できるようになっています。

先程のサンプルコードを再掲します。

import akka.actor.typed.scaladsl.{ Behaviors }
import akka.actor.typed.{ ActorRef, ActorSystem, Behavior }

trait Message
case object Print extends Message
case class Update(numValue: Int) extends Message

object MyTypedActor {
  def apply(numValue: Int): Behavior[Message] = // (1)
    Behaviors.receive[Message] { (ctx, msg) =>  // (2)
      msg match {
        case Print =>                           // (3)
          println(numValue)
          Behaviors.same
        case Update(newNumValue) =>             // (4)
          MyTypedActor(newNumValue)
      }
    }
}

(1) MyTypedActornumValue という Int のプロパティを持つ Actor です。しかしコード内には numValue を保持するためのインスタンス変数が存在していません。

(2) apply() 関数の中で呼び出されている Behaviors.receive が Actor を生成する関数です。receive の引数には Actor の処理を関数として渡しています。関数が受け取る引数は ActorContextMessage になります。ActorContextActorSystem を参照したりロガーを取得したり子 Actor を生成・参照したりなど各種 Actor に関する操作を行うために使用できます。

(3) MessagePrint だった場合は、最後に Behaviors.same を返しています。これは Actor の状態を変更せずそのままその Actor を再利用するということを意味します。

(4) 一方で MessageUpdate だった場合は Behaviors.same ではなく MyTypedActor を新しい値で新たに生成して返しています。こうすると Actor はその新しいものに差し替えられます。

Actor 本体は関数として定義しているためプロパティを持たせることはできません。その代わりに新しいパラメータで関数を再帰的に呼び出すことで Actor の状態を更新していくことができるという仕組みになっているのです。このように Actor としては従来通りステートフルでありながらコードの表現上は Immutable に記述することができます。

Classic Actor から移行する

ありがたいことに Typed Actor は Classic Actor と共存できるように作られています。そのため Typed Actor を部分的に導入したり段階的に移行したりといったことが可能です。

Classic な ActorSystem から Typed Actor を生成する方法は極めて簡単です。

import akka.{ actor => classic }
import akka.actor.typed.scaladsl.adapter._

val system = classic.ActorSystem("classic-system")

val actor: ActorRef[Message] =
  system.spawn(MyTypedActor(123), "my-actor")

actor ! Print

akka.actor.typed.scaladsl.adapter._ というアダプターを import すると ActorSystem から spawn メソッドで Typed Actor が生成できるようになります。生成した Actor はそのまま普通に Typed Actor として使用できます。

これにより、既存の Classic Actor ベースのコードにほとんど手を加えることなく、新規に追加するコードは Typed Actor で記述するといったことが可能になります。Classic Actor と Typed Actor では記述方法がだいぶ異なってくるため、一気に移行するよりもそのようにまずは新規追加のコードから導入して少しずつ移行していくのがよいのではと思います。

逆に Typed Actor の環境から Classic Actor を生成して扱うこともできます。このあたりの相互運用については以下の公式ドキュメントで具体的に説明されています。

Coexistence • Akka Documentation
https://doc.akka.io/docs/akka/current/typed/coexisting.html

さいごに

非常に簡単ではありますが Akka Typed への移行について紹介させていただきました。Akka Typed を導入することで、Akka が提供する先進的な非同期分散処理と Scala の Type Safe で Immutable なコードと組み合わせて、アプリケーションの機能をより堅牢に簡潔に実現することができます。

Classic Actor とは違う点がいくつかあるため注意が必要な部分もありますが、公式にも Typed Actor を推進していく流れのようなので、我々としても引き続き移行を進めていきたいと考えています。

以上となります。最後までお読みいただきありがとうございました。