アクタ
メッセージはクラス
メッセージは普通の値クラスです。ハンドラは、そのメッセージクラスの名前を持つアクタのメソッドです。
Object subclass: #Add fields: (item : Str).
Add class >> of: s = Add fields: { item: s }.
Add >> item = item.
Object subclass: #Count.
Actor subclass: #Cart fields: (items : Array[Str]).
Cart class >> new = Cart fields: { items: #[] }.
Cart >> Add m = self copyWith: { items: items copyWith: m item }.
Cart >> Count = self reply: items size.
メソッド名が大文字で始まればメッセージクラスの名前で、そのメソッドはハンドラです。メッセージに読むフィールドがあるときは引数で受け取り(Add m)、フィールドが無いときは引数を書きません(Count)。小文字で始まるメソッド名は普通のメソッドで、そのアクタ自身のコードだけが呼びます。
どのメソッドがハンドラかは、ソースに書いてあります。戻り値から推論するのではありません。
ハンドラは次の状態を返す
ハンドラが返せるのは次の五つです。
| 式 | 次の状態 | 返事 |
|---|---|---|
self | 変わらない | — |
self copyWith: { ... } | 指定したフィールドを差し替える | — |
self become: other | 別のインスタンス。クラスが違ってもよい | — |
self reply: v | 変わらない | v |
self reply: v with: { ... } | 指定したフィールドを差し替える | v |
それ以外を返すと、定義の時点でコンパイルエラーです。
Inc answers Int, and a handler answers the next state — `self`, `self copyWith:`,
`self become:` or `self reply:`
フィールドへの代入構文はありません。アクタの状態が変わるのは、ハンドラが新しい状態を返したときだけです。
ハンドラに入れるのはメールボックスだけです。 大文字で始まる名前はセレクタではないので、(Counter init: 0) Inc は構文として書けません。ハンドラを直接呼んで、返ってきた状態を捨てる書き方が存在しません。
普通のメソッドには制限はありません。大きな画面には補助のメソッドが要るので、そちらは自由に呼べます。
送信は ! と ask:
c := Actor spawn: Cart new.
c ! (Add of: '六角ボルト'). "送るだけ。返事を待たない"
(c ask: (Count fields: {})) forced displayNl. "返事を待つ"
! と ask: はメッセージを受け取ります。どのハンドラに届くかは、メッセージのクラスで決まります。! は参照を返すので送信を続けて書けます。ask: は Future を返し、forced で返事を待ちます。フィールドの無いメッセージクラスは fields: {} で作ります。
そのメッセージのハンドラが無ければ、送信はコンパイルエラーです。
actor Counter has no handler for Nope
ハンドラの中で forced を使うと、そのアクタは返事が来るまで次のメッセージを処理しません。他のアクタは動き続けます。計測では、100ms かかる問い合わせを三件送ったアクタは 104ms、207ms、320ms の時点で順に処理し、その間も別のアクタは 3ms、70ms、136ms で動作していました。
メッセージは送った順に届きます。待っている間に届いたメッセージは、返事のあとで順に処理されます。スケジューラは 32 件ごとにマクロタスクへ譲るので、忙しいアクタがタイマーや I/O を止めることはありません。
返事を待たない書き方
サーバの返事を待たずにハンドラを終え、返事を自分宛のメッセージとして受け取る書き方です。
Object subclass: #Load.
Object subclass: #Loaded fields: (res : Response).
Loaded class >> of: r = Loaded fields: { res: r }.
Loaded >> res = res.
Screen >> Load = (
(Http get: 'orders.json') then: [:r | me ref ! (Loaded of: r) ].
self ).
Screen >> Loaded m = self copyWith: { rows: ... }.
Load はすぐ終わるので、通信中も画面は操作を受け付けます。返事は Loaded メッセージとして届きます。WebSocket も同じ形で書きます。
ask: 自体は待ちではありません。待つのは forced です。上のコードは turn を終えて答えを後の turn に渡すので、アクタはメッセージを受け続けます。いま聞いた相手からのメッセージも受けられます。
待つメソッドは Future を返すので、答えを使う場所で forced を忘れれば型エラーになります。文として書いたときは型が読まれないので、値が Future の文には警告を出します。
this statement answers a Future and nothing waits for it — write `forced` to wait
here, or `then:` to say where the answer goes
then: onError: map: で終わる文には警告を出しません。答えの行き先が書いてあるためです。拒否ではなく警告なのは、答えを読まない要求を書くのが正しい場合もあるからです。
エラーを起こす・受け止める
エラーを起こすのは Error raise: です。その場で受け止めるのは Error try:onError: で、受け止める側のブロックにエラーのメッセージが渡り、その答えが式の値になります。本体と受け止める側は同じ型を返します。
(Error try: [Check positive: n] onError: [:msg | 0]) displayNl.
言語が起こすエラーも同じように受け止めます。空の配列への first などです。待つ本体は Future を返すので、その Future が後で失敗した場合も同じように受け止めます。
aBlock ensure: cleanup は、ブロックがどう終わっても後始末を実行し、ブロックの答えを返します。待つブロックの場合は、答えが決まった時点で後始末を実行します。
ハンドラがエラーを出したとき
受け止めずにエラーが起きると、そのアクタは止まります。監視しているアクタが受け取るメッセージは Down です。
Boss >> Start = ( me monitor: worker. self ).
Boss >> Down d = self copyWith: { log: (log copyWith: d actor name , ' が止まった') }.
me monitor: worker と書いておくと、worker が止まったときに Down メッセージが届きます。me linkTo: worker にすると相手が止まったとき自分も止まり、me trapExits: true を指定してあれば止まる代わりに Exit メッセージが届きます。監督役は、Down を受け取って代わりのアクタを spawn: するアクタです。
止まるのはそのアクタだけです。他のアクタも、画面の他の領域も影響を受けません。
止まったアクタが描いていた領域は、DOM が最後に描いたまま残ります。ボタンをクリックしても、届けるアクタがないので何も起きません。そこで、止まったアクタの領域には「エラーで止まりました」という案内が表示されます。文面は、ページに id="lps-dead" の要素があればその中身を使います。無ければ標準の案内を表示します。標準の案内は色や配置を自分で持つので、ページの CSS が無くても表示されます。
案内は領域の上に表示され、それまでの表示内容は下に残ります。利用者がエラーの直前に何をしていたかが分かるようにするためです。
案内が出るのはエラーで止まったときだけです。me stop で意図的に終了したときには出ません。
become: によるクラスの切り替え
become: を使うと、アクタは自分自身を別のクラスのインスタンスに置き換えられます。それ以降のメッセージは、新しいクラスのメソッドで処理されます。状態によって受け付けるメッセージの種類が変わる場合に使います。
Object subclass: #Open.
Object subclass: #Close.
Object subclass: #State.
Protocol subclass: #Door requires: (Open Close State).
Actor subclass: #ClosedDoor protocols: (Door).
Actor subclass: #OpenDoor fields: (opens : Int) protocols: (Door).
ClosedDoor >> State = self reply: 'closed'.
ClosedDoor >> Open = self become: (OpenDoor fields: { opens: 1 }).
ClosedDoor >> Close = self.
OpenDoor >> State = self reply: 'open (' , opens asString , ')'.
OpenDoor >> Open = self copyWith: { opens: opens + 1 }.
OpenDoor >> Close = self become: (ClosedDoor fields: {}).
アクタ向けのプロトコルは、requires: にメッセージクラスを並べます。各段階に protocols: (Door) と書いておけば、どの段階も Open、Close、State を処理することをコンパイラが検査します。
参照は、spawn したクラスの型のままです。Actor spawn: (ClosedDoor fields: {}) の型は ActorRef[ClosedDoor] で、ClosedDoor が受け付けるメッセージなら何でも送れます。だから置き換わった先のクラスもそれを全部受け付けなければなりません。足りなければコンパイルエラーです。
d := Actor spawn: (ClosedDoor fields: {}).
(d ask: (State fields: {})) forced displayNl.
d ! (Open fields: {}).
参照のクラスが決まらない場合、その送信は実行時に解決されます。子が親への参照を持つのはこの形です。子を別のモジュールに置くと、子は親のクラス名を書けません。親が子を import しているので、子から親のクラス名を書けば import が循環するためです。
Actor subclass: #Kid fields: (label : Str, owner : ActorRef[a]).
Kid class >> named: n owner: o = Kid fields: { label: n, owner: o }.
Kid >> Report = ( owner ! (Noticed of: label). self ).
Desk >> Hire m = self copyWith: {
kids: kids copyWith: (me spawn: (Kid named: m name owner: me ref)) }.子アクタの参照は親のもの
あるクラスが me spawn: だけで生成され、それが一つのアクタクラスの中だけなら、そのクラスはそのアクタの子です。コンパイラは、他のアクタがその子への参照を持つことを拒否します。フィールドに持つ場合、! や ask: の引数に渡す場合、返事に載せる場合のいずれもです。配列、Maybe、値クラスの中まで検査します。
Panel holds a reference to Panel in its field next, but Panel is a child of Wall —
a child's reference belongs to the actor that spawned it, because a restarted child
is a new reference that only its parent hears about (§5.10)
理由は再起動です。子が止まって親が作り直すと、それは新しい参照になります。知らされるのは親だけです。古い参照を持った兄弟は、存在しないアクタへ送り続けることになります。止まったアクタへの送信はエラーにならず捨てられるので、気付けません。
最上位で Actor spawn: や App mount: で生成したクラスは子ではありません。作り直す親がいないためです。
デッドロックの検出
二つのアクタが互いの返事を待つと、どちらも動けなくなります。どちらもエラーで止まったわけではないので、monitor: では検出できません。
forced は、待つ前に、各アクタが何を待っているかを辿ります。辿った先が待とうとしているアクタに戻ってきたら、エラーにします。
Pong#2 asked Ping#3, which is waiting on it — nobody can take a turn
検出されるのは実際のデッドロックだけです。誤検出はありません。Future all: を forced すると複数の返事を同時に待つので、アクタが待っている相手は集合になり、探索はそのすべてを対象にします。
Worker を跨いだデッドロックも検出します。返事を forced する ask: は、その時点で待っているアクタのアドレスと名前を一緒に送ります。受け取ったスレッドは、送り先がその一覧に含まれていればエラーにします。判断は受け取ったスレッドだけで行い、相手のスレッドへの問い合わせはしません。
Far#1 asked Home#1, which is waiting on it — nobody can take a turn
ask: を名前に束縛して後の行で forced する書き方は、スレッドを跨いだ検出には乗りません。同じスレッド内の検出は効きます。