アクタ

アクタの定義

変化する値(一覧の内容、入力途中の文字列など)を持てるのはアクタだけです。アクタはクラスとして定義します。フィールドに持つ値を宣言し、受け取れるメッセージをメソッドとして書きます。

Actor subclass: #Cart fields: (items : Array[Str]).

Cart class >> new   = Cart fields: { items: #[] }.
Cart >> add: item   = self with: { items: items copyWith: item }.
Cart >> count       = self reply: items size.

Cart は文字列の配列 items を持ちます。add: メッセージを受け取ると、items に一件追加した新しい状態を返します。count を受け取ると、件数を返信します。

メソッドがそのままメッセージハンドラです。ハンドラは次の状態を返します。self with: { ... } は、指定したフィールドだけを差し替えた新しい状態を返します。元の状態は変わりません。返信が必要なハンドラは self reply: を使います。

フィールドへの代入構文はありません。状態を変えるには、self with: で新しい状態を作って返します。アクタの状態が変わるのは、ハンドラが新しい状態を返したときだけです。

tell と ask

c := Actor spawn: Cart new.

c tell add: '六角ボルト'.              "送るだけ。返事を待たない"
(c ask count) forced printNl.          "返事を待つ"

tell はメッセージを送るだけで、返事を待ちません。ask は返事を要求する送信で、Future を返します。forced で返事を待ちます。

ハンドラの中で forced を使うと、そのアクタは返事が来るまで次のメッセージを処理しません。他のアクタは動き続けます。計測では、100ms かかる問い合わせを三件送ったアクタは 104ms、207ms、320ms の時点で順に処理し、その間も別のアクタは 3ms、70ms、136ms で動作していました。

メッセージは送った順に届きます。待っている間に届いたメッセージは、返事のあとで順に処理されます。

返事を待たない書き方

サーバの返事を待たずにハンドラを終え、返事を自分宛のメッセージとして受け取る書き方です。

Screen >> load = (
  (Http get: 'orders.json') then: [:r | me ref tell loaded: r].
  self ).

Screen >> loaded: r = self with: { rows: ... }.

load はすぐ終わるので、通信中も画面は操作を受け付けます。返事は loaded: メッセージとして届きます。WebSocket も同じ形で書きます。受信のコールバックでは me ref tell arrived: のように自分宛にメッセージを送ります。

ハンドラの戻り値は捨てられない

ハンドラの戻り値は、アクタの次の状態です。ハンドラを普通のメソッドのように呼んで戻り値を捨てる書き方は、コンパイルエラーになります。

Counter >> inc = self with: { count: count + 1 }.

Counter >> bump = self inc.              "可。inc の戻り値がそのまま bump の戻り値になる"

Counter >> reset = (
  self inc.                              "コンパイルエラー。inc の戻り値が捨てられている"
  self with: { count: 0 } ).
"inc" on Counter is a handler, so what it answers is the next state — here
that answer is dropped. Put the send where this handler answers, or send it
as a message with `me ref tell inc`

別のハンドラの戻り値をそのまま自分の戻り値にする書き方(bump)は可です。途中で呼んで戻り値を捨てる書き方(reset)は不可で、そうしたい場合は me ref tell inc と自分宛のメッセージとして送ります。

このエラーが無いと、reset の self inc は状態を変えず、エラーも出ないまま実行されます。それを防ぐための検査です。

ハンドラでない普通のメソッド(画面を組み立てる補助メソッドなど)には、この制限はありません。

エラーで止まったとき

try はありません。ハンドラの中でエラーが起きると、そのアクタは止まります。

Boss >> start   = ( me monitor: worker. self ).
Boss >> down: d = self with: { log: log copyWith: d actor name , ' が止まった' }.

me monitor: worker と書いておくと、worker が止まったときに down: メッセージが届きます。link: にすると、相手が止まったとき自分も止まります。監督役を作るには、down: を受け取って代わりのアクタを spawn: するアクタを書きます。

止まるのはそのアクタだけです。他のアクタの状態も、画面の他の領域も、影響を受けません。

止まったアクタが描いていた領域は、DOM が最後に描いたまま残ります。ボタンをクリックしても、止まったアクタにはメッセージが届かないので何も起きません。そこで、止まったアクタの領域には「エラーで止まりました」という案内が表示されます。文面は、ページに id="lps-dead" の要素があればその中身を使います。無ければ標準の案内を表示します。標準の案内は色や配置を自分で持つので、ページの CSS が無くても表示されます。

案内は領域の上に表示され、それまでの表示内容は下に残ります。利用者がエラーの直前に何をしていたかが分かるようにするためです。

案内が出るのはエラーで止まったときだけです。me stop で意図的に終了したときには出ません。

become: によるクラスの切り替え

become: を使うと、アクタは自分自身を別のクラスのインスタンスに置き換えられます。それ以降のメッセージは、新しいクラスのメソッドで処理されます。状態によって受け付けるメッセージの種類が変わる場合に使います。

Door >> open = self become: (OpenDoor fields: {}).

置き換わった先のクラスがどのメッセージを受け付けなければならないかは、参照の型で決まります。

Actor spawn: で作った参照は ActorRef[Door] です。この参照には Door が受け付けるメッセージなら何でも送れるので、置き換わった先のクラスもそれを全部受け付けなければなりません。足りなければコンパイルエラーです。

Actor spawn:as: でプロトコルとして作った参照は、そのプロトコルのメッセージしか受け付けません。置き換わった先のクラスは、プロトコルの分だけ実装すれば済みます。

spawn:as: は、子を作る側が参照をプロトコルの型にする書き方です。ref as: P は、自分の参照をプロトコルの型にして相手に渡す書き方です。子を別のモジュールに置くと、子は親のクラス名を書けません。親が子を import しているので、子から親のクラス名を書けば import が循環するためです。そこで親は me ref as: Owner を渡します。子は Owner というプロトコルだけを知り、子から親へのメッセージはそのプロトコルに対して型検査されます。

Desk >> hire: n = self with: {
  kids: kids copyWith: (me spawn: (Kid named: n owner: (me ref as: Owner))) }.

ActorRef[a] としか型の付いていない参照には、メッセージ名を照合する相手のハンドラがありません。そこへの tell はコンパイルエラーになります。

nothing says which actor "nosuchThing:" is sent to, so it cannot be checked —
type the reference by a protocol: `ActorRef[P]`, made with `ref as: P` or `spawn:as:`

デッドロックの検出

二つのアクタが互いの返事を待つと、どちらも動けなくなります。どちらもエラーで止まったわけではないので、monitor: では検出できません。

そこで ask は、送る前に相手が誰を待っているかを辿ります。辿った先が自分に戻ってきたら、その場でエラーにします。

Pong#2 asked Ping#3, which is waiting on it — nobody can take a turn

検出されるのは実際のデッドロックだけです。推測でエラーにすることはありません。

Worker を跨いだデッドロックも検出します。別スレッドへの ask は、その時点で待っているアクタのアドレスと名前を一緒に送ります。受け取ったスレッドは、送り先がその一覧に含まれていればエラーにします。判断は要求を受け取ったスレッドだけで行い、相手のスレッドへの問い合わせはしません。

Far#1 asked Home#1, which is waiting on it — nobody can take a turn