Akka.jsについて
Scala.jsでAkka出来るライブラリとしてAkka.jsがあります。
https://github.com/akka-js/akka.js
2019年10月時点では、Akkaのバージョン2.5.23に対応していて、akka-streamやTypedなActorもサポートしています。
コードベース自体は本家のものをそのまま流用しつつ、JVMとJSとの差異部分にパッチを当てるような感じで実装されているので、原則的には本家のAPIとそのまま互換性があります(JSの制限上、実現不可能なものについてはその限りではない)
では、Akka.js(Typed)で何ができるのか?
ざっくりといいますと、型付のActorモデルのパラダイムを利用した実装をJS側でも使える上に、場合によってはJVM側と同じコードを共有できます。
Typedについて
これまでのAkkaのActorはメッセージングの部分に型のサポートが効かない(メッセージがAny)ことで、コンパイラの加護を受けられず、網羅的なコードを書くことが難しいため使いにくさがありました。
が、Typedになったことでこの問題が解消し、より安全にActorを使うことが出来るようになっています。
既存のAkkaにおけるActorとTypedの場合の違いは、こちらの記事が分かり易いかと思います。
https://todokr.hatenablog.com/entry/2019/10/23/002117
AkkaのActorモデルについて
Actorは独立したスレッドセーフな状態を保持し、外部とのやりとりはメッセージを介してのみ行います。これにより、利用者は複雑なマルチスレッドプログラミングから解放され、プログラムをActorという小さな部品の集合として考えることが出来るようになります。
また、Actorは一意なアドレスにより識別されていて、目的のActorのアドレスが手元にあれば、どこからでもメッセージを送信することができます。
これは、サーバサイドにおける並行/並列、非同期分散の処理において非常に有益であることはすでに周知のこととは思いますが、ではJSではメリットがあるのでしょうか?
JSにおけるAkka
JSはご存じの通り、シングルスレッドで動いています。Akkaの長所のひとつであるマルチスレッドプログラミングにおける優位性は得られませんが、それでもなお、有益なのでしょうか?
Akka.jsのユースケースをまとめて下さっている方がいましたので、気になる方は見てみてください。
https://github.com/akka-js/akka.js/issues/12
上記も参考にしつつ、ブラウザ上でAkkaを使うユースケースを少し考えてみました。
副作用の置き場として
Actorは独立して隔離されているため、外から直接状態を参照したり、操作することは出来ません。
つまり、状態の変更を伴うような処理を書いたとしても、Actorの内部にそれを隠蔽し、外部に漏らさない構造に出来ます。
immutableなデータ構造を取り扱った場合でも、どこかでその変更を保持する場所は必要です。Actorは、その受け口としての選択肢に適していると思います。
JSにおいては、どうしてもロジックがUIに引っ張られがちになりますが、Actorに包むことで、明確な境界を引き、堅牢な状態の分離を実現する事ができます。
ステートマシン
状態変化とライフサイクルを持ったモデルをコード化するのは複雑な仕事です。外部からの操作の作用はそのときのモデルの状態に依存します。
複雑なUIを伴うアプリケーションの場合、その傾向はいっそう顕著ではないでしょうか。UIと状態の分離が望ましいとは思いつつ、状態を切り分けると、ひとつの巨大なデータ構造になって手に負えなくなったりします。
AkkaはActorをステートマシンとして使うことで、複雑な状態変化とそれぞれのステートに付随した処理を伴ったモデルを比較的容易に表現できます。
TypedなAkkaの場合、超ざっくり言ってしまうと、Actorの実体は、その振る舞いを閉じ込めたBehavior[T]
です。これはT
という型のメッセージを受け取り、何らかの処理を行なって、結果として次のBehavior[T]
を返します。
公式ドキュメントからコードの一部を拝借いたします。
https://doc.akka.io/docs/akka/current/typed/actors.html#introduction
val greeter: Behavior[Greet] = Behaviors.receive { (context, message) =>
context.log.info("Hello {}!", message.whom)
Behaviors.same
}
Behaviors.same
の行が、新しいBehavior[T]
を返している部分です。sameの場合は同じBehavior[T]
にとどまることを表していますが、ここで別なBehavior[T]
を返すことで、振る舞いを遷移させることができます。
また、このコードではgreeter
はval
であり、特に状態を要求していませんが、例えば
def greeter(myName: String): Behavior[Gree] = ...
のようにして、何らかの状態を引数に取るようにすると、このBehavior[T]
を生成する際にそれを外から渡すことが出来るようになり、その振る舞いにとどまる間は参照することが出来るようになります。
この振る舞いを続けながら状態を変化させたければ、新しいBehavior[T]
を返すときにdef greeter
に渡す値を変化させればいいわけです。
こうすると、状態に応じたメッセージの処理に注力できます。
状態変化の境界がはっきりすることで、複雑な状態遷移も比較的取り扱い易いのではないでしょうか。
ちなみに、このようなパターンはTypedなAkkaではなくても使えます。
https://doc.akka.io/docs/akka/current/actors.html#become-unbecome
非同期イベントハンドリング
JSは、シングルスレッドの宿命として、ブロッキングが許されません。そのため、すべての処理が注意深くコールバックにより構築され、ブロックしないように設計されています。
それに伴い、何かしらの非同期処理をおこなうと、コールバックや、イベントハンドリングが頻繁に発生し、気をつけないと地獄に落ちます。
JSにおいてはasync/await
によるアプローチは非常に強力ですが、Actorモデルはまた違ったアプローチを提供します。
それが、メッセージングによるイベントのハンドリングです。
ざっくりとした考え方の違いですが、async/await
は、中断/再開によってある文脈の途中に非同期な処理を組み込めるようにしていますが、アクターモデルの場合、Actorという閉じたモデルの境界間を非同期なメッセージで繋ぐことで、モデル単位での非同期処理を実現しています。(この辺りはツッコミをいただけると幸いです)
Actorモデルの思想としてメッセージは原則「投げっぱなし」であることが特徴です。送信側は基本的には返信を受け取ることはありません(明示的に返信を受け取るパターンもあるものの、それを前提にするのは好ましくない)
イベントを待ち受けたい場合にどうするかというと、通常はPub/Subのパターンを利用します。
イベントの発行者に対して購読を申し込む(このイベントが発行されたら僕に通知してね、という申し込みメッセージを送信する)
いわば、メルマガの購読ですね。メッセージはメールのようなもの、と捉えると分かり易いかと思います(もう少し付け加えると、メッセージは順序付きで、FIFOなQueueの構造に格納される)
ところで、TypedなAkkaにおいては、メッセージの送信元を暗黙的に取得することができません。
何かしらの返信が欲しい場合には、明示的に返信先をメッセージに含めて送信する必要があります。
(ClassicなActorではコンテキストからsender
を取得できていました)
個人的にはメッセージの宛先は明示する方が処理の意図も明確になるので好ましいかなと思っております。
外部との通信の抽象化
先述のユースケースまとめの中にWebSocketやバックグラウンドのワーカーを利用するパターンがあったように、それらの外部とのやりとりをAkkaで包んでしまうことが出来ます。
ありがたいことに、Akka.jsはakka-streamをサポートしていますので、非同期に連続して流れてくるメッセージを処理するのにも適しています。
例えば、WebSocketから流れてきたメッセージを、デシリアライズする、フィルタする、振り分ける、エラーハンドリングする...、と言った一連の処理を、ステージごとに分割して管理できます。
Akka.jsの例ではないのですが、akka-streamとOpenCVで画像処理をする記事がとても面白かったので、参考までに貼っておきます。
https://beachape.com/blog/2016/03/08/scala-and-opencv-ep-1-akka-webcam/
JSでも、よりリアルタイム性の高いアプリケーションが求められるようになり、非同期な通信やメディアのストリーミングなども当たり前のように要求されます。
こういった外部との通信を円滑にシステムに統合するのにもAkkaは役立ちます。
レイヤ間の境界の厳格化
なんらかのアーキテクチャ(例えばレイヤードとか、ヘキサゴナルとか、クリーンとか、その辺からヒントを得て派生した何かとか)を採用している場合、各層の境界をコード上で厳格に表現する手段のひとつとして、それぞれの層を別々のActorとして実装するという選択肢があります。
Typedであれば、各層間を移動するデータには型がつきますし、安易なレイヤー違反も防げます。また、各層で独自に状態を保持する必要性があったとしても、それは安全に隔離されるので、他から誤って参照されて、依存関係が壊れる...のような事態も避けられます。
これが、マルチスレッドをサポートした環境下であった場合は、例えば描画はUIスレッドでなければならないなどの制約があり、注意が必要ですが、JSであればその辺は特に気にする必要がありません。
あとがき
ざっくりと、Akka.jsの紹介と自分の考えるユースケースを書いてみました。
アクターモデルというスタイルは、JSにおいても有益だと思います。
また、もちろんですが、TypedなAkkaはJVMでも非常に強力です。
ぜひ、触ってみてください。