アクターは Lambda Prelude における常駐サービスの単位。OCaml 5 のエフェクトハンドラ上に直接実装されている — Eio・Miou・POSIX 専用 API には依存しない。同じアクターコードが Windows と Linux、インタプリタと AOT で動く。

3 つの通信プリミティブ

アクター表層は spawn / send / receive の 3 つの通信プリミティブと、自分自身への参照を返す Actor self から始まる。

Actor self                  "現在の fiber のアクター参照"
Actor spawn: [:me | body]   "新規 fiber を生成、body は自身の参照を受ける"
actor ! message             "fire-and-forget 送信"
actor receive               "メッセージが来るまでブロック"

最初の ping-pong は次のとおり。

parent := Actor self.

child := Actor spawn: [:me |
  msg := me receive.
  msg printNl.
  parent ! #pong
].

child ! #ping.
parent receive printNl.

状態はループ引数として生きる

共有可変状態はない。アクターの状態は再帰ループの引数として表現する。「状態を更新する」とは、新しい値で自分自身を呼び直すこと。

Object subclass: #Counter.

Counter class >> run: me count: n =
  me receive: {
    Inc      -> [:msg | Counter run: me count: n + 1].
    GetCount -> [:msg | msg reply ! n. Counter run: me count: n]
  }.

counter := Actor spawn: [:me | Counter run: me count: 0].

n は呼び出しフレームの中に閉じている — 他の fiber から読み書きされない。

型付き選択受信

me receive: { Class -> [:msg | ...] } はクラス・タグディスパッチ表。スケジューラがメールボックスを先頭から末尾へ走査し、最初に一致したハンドラを実行する。一致しないメッセージは元の位置に残る — これが選択受信。

Object subclass: #Inc.
Object subclass: #Get fields: (reply).

Counter class >> run: me state: n =
  me receive: {
    Inc -> [:msg | Counter run: me state: n + 1].
    Get -> [:msg | msg reply ! n. Counter run: me state: n]
  }.

ディスパッチ表は各メッセージクラスを Hindley-Milner 推論に通すので、ハンドラ本体は msg を具体型として扱える — Obj.magic も型を回避する抜け穴もない。

受信タイムアウト

receive:after:do: は一致したメッセージかタイムアウト分岐のどちらかを返す。

me receive: { Tick -> [:t | handle: t] }
   after: 1000
   do:    [Log warn: 'no tick for 1 s'].

Future と ask:

ask: は send + receive の上に乗った request / response の糖衣。応答が到着したときに解決する Future を返す。

fut := worker ask: (Compute fields: { x: 7 }).
fut await printNl.

Future の手動生成も用意してあるが、通常経路は ask:

リンク / モニタ / 終了シグナル捕捉

失敗の意味論は Erlang の語彙をローカルモデルに当てはめたもの。

Object subclass: #Boom.
Object subclass: #Worker.

Worker class >> loop: me =
  me receive: {
    Boom -> [:msg | Error raise: 'kaboom!']
  }.

main   := Actor self.
worker := Actor spawn: [:me | Worker loop: me].

main monitor: worker.
worker ! (Boom fields: {}).

main receive: {
  Down -> [:d |
    'Down received' printNl.
    d reason printNl
  ]
}.

'main still alive' printNl.

Down は死んだアクターの参照と失敗理由を運ぶ。観察側は走り続ける — リンクを貼っていない限り連鎖死は起きない。

スーパーバイザ

スーパーバイザツリーは専用構文ではなく、ふつうのクラスとして書く。スーパーバイザは子を監視し、Down メッセージを契機に再起動する。再起動予算を使い切った場合は停止し、上位スーパーバイザに監視されていればそこで扱われる。

stdlibSupervision モジュールは再利用可能な戦略 (one-for-one / one-for-all / rest-for-one) と再起動ポリシーを提供するが、内部機構は monitor: + spawn: の組合せに過ぎない。

マルチドメインスケジューリング

ランタイムはマルチドメイン構成 — アクターは OS スレッドをまたいで spawn でき、クロスドメインの inbox 受け渡しとワークスティーリングが効く。AOT バイナリには同じランタイムが一緒にコンパイルされているので、まったく同一のマルチドメインスケジューラを備える。CPU バウンドな仕事はどちらのモードでもコア間に拡がり、別個の単一ドメインランタイムは存在しない。

境界つきソケット I/O

TCP connect:port:timeout:TCPSocket >> readLine:timeout: / readBytes:timeout:TLS wrapClient:host:timeout:、TLS の read / write はすべてアクタースケジューラと統合されている。遅い上流は呼び出し側 fiber を wait_readable / wait_writable で待機させるだけで、OS スレッドはブロックしない — 他のアクターは走り続ける。

TLS ブリッジは Tls.Engine を直接駆動する — ハンドシェイク / アプリケーション read / write がそれぞれランタイムの I/O wait プリミティブの上で待機する。

ロードマップから外しているもの

Erlang / Akka Cluster 形式の分散アクター — ノード間 spawn / link / monitor / スーパーバイザのワイヤ越し版 — はロードマップ。ローカルアクターモデルはそのまま維持する。

複数ノード対応 — 仮想アクター (grain)

複数ノード方式は仮想アクター (grain) として実装済み。grain は fiber 参照ではなく安定した (クラス, id) の組で宛先指定されるアクターで、ライブ状態は固定したプロセスメモリではなく外部ストアに置く。ネットワーク上のオブジェクト参照ではなく (クラス, id) で到達するので、アイドル時は活性化を落とし(メモリを解放し)、次の呼び出しで外部ストアから状態を読み直して再活性化でき、GC すべきノード間参照グラフが存在しない。アプリケーションは grain クラスに通常のメソッドを定義し、その (クラス, id) 宛てにメッセージを送る — 分散ライフサイクルは stdlib/grain.lpGrain モジュールが担う。

ランタイムは「(クラス, id) ごとに生きたオブジェクトは 1 つ」を 3 つの役割で保つ:

単一ノードモード (for:initial:redis:) はディレクトリとオーナのみ、クロスノードモード (for:initial:redis:url:) がリースを足す。id を所有しないノードはオーナを名指すエラーを返し、クライアントが再試行をルーティングする (サーバ側の透過転送は未実装)。Redis は揮発的な活性化ストアであって永続的な記録ではない — system-of-record DB への永続化はアプリケーションの責務。

ストアは差し替え可能。GrainServerGrainStore プロトコル (stateFor: / persist:json:token: / acquireFor: / renew:token: / release:token:) 越しに話す。RedisGrainStore が組込バックエンドで、アプリケーションは独自ストア (例: アクター保持のインメモリマップ) を for:initial:store: で注入できる。

選択受信 / リンク / モニタはあくまでローカル限定 — ノードをまたいだリンク連鎖は導入しない。遠隔参照 Remote at: url for: #Class id: はリテラルのクラスを渡すと型に乗り、未知セレクタはコンパイル時エラーになる (非リテラルの for: は従来通り動的型付け)。