データと挙動の共有

Motoko では、ミュータブルなステートは常にプライベートであることを思い出してください。

しかしながら、2 つの Actor はメッセージによってデータを共有することができ、それらのメッセージは自分自身とお互いを含む Actor を参照することができます。 さらに、shared 関数であれば、メッセージが個々の関数を参照することが可能です。

これらのメカニズムにより、2つの Actor は非同期のメッセージパッシングによって挙動を協調させることができます。

Actor による出版-購読型(Publisher-subscriber)パターン

この章の例では、 出版-購読型パターン のいくつかの例に焦点を当て、Actor がどのように関数を共有するかを説明します。 出版-購読型パターンでは、出版する(publishing) Actor は、購読する(subscriber) Actor のリストを記録して、出版者のステートに何らかの変化があった際に通知します。 例えば、出版者の Actor が新しい記事を発行すると、購読者の Actor に対して新たな記事があることが通知されます。

以下は 2 つの Actor を使って Motoko で出版-購読型の関係を構築する例を示します。

このパターンを使用するプロジェクトの全体のコードを見るには、サンプルリポジトリpubsub をご覧ください。

購読者(Subscriber) Actor

以下の Subscriber Actor の型は、出版者 Actor から呼び出すことができるように購読者 Actor が公開しているインターフェースです。

type Subscriber = actor {
  notify : () -> ()
};
  • Publisher はこの型を、購読者をデータとして保持するデータ構造を定義するために使います。

  • それぞれの Subscriber Actor は、上の例で型シグネチャが示しているように、更新用の関数である notify を公開しています。

サブタイピングによって、Subscriber Actor は上の型定義で示されていない追加のメソッドを含むことが可能であることに注意してください。

問題を単純にするため、notify 関数は関連する通知データを受け取り、購読者に関する何らかの新しいステータスメッセージを出版者に返すことにしましょう。 例えば、購読者は通知されるデータに基づき、購読に関する設定の変更を返すかもしれません。

出版者(Publisher) Actor

出版者側は購読者の配列を持ちます。 問題を単純にするため、それぞれの購読者は subscribe 関数を用いて一度だけ購読できるものとしましょう。

import Array "mo:base/Array";

actor Publisher {
    var subs: [Subscriber] = [];

    public func subscribe(sub: Subscriber) {
        subs := Array.append<Subscriber>(subs, [sub]);
    };

    public func publish() {
        for (sub in subs.vals()) {
          sub.notify();
        };
    };
};

その後、ある外部のエージェント(agent)が publish 関数を呼び出すと、上で述べた Subscriber 型で定義されている notify メッセージをすべての購読者が受け取ります。

購読者メソッド

最も単純なケースでは、購読者 Actor は以下のメソッドを持ちます。

  • init メソッドを用いて出版者からの通知を購読する。

  • 購読者 Actor の一人として、上記 Subscriber 型の notify 関数で指定された通知を受け取る。

  • 蓄積したステートに対する問い合わせを許可する。このサンプルコードでは受け取った通知の数を保存する count 変数に対する get メソッドがそれに該当する。

次のコードは、これらのメソッドを実装した例です。

actor Subscriber {
  var count: Nat = 0;
  public func init() {
    Publisher.subscribe(Subscriber);
  };
  public func notify() {
    count += 1;
  };
  public func get() : async Nat {
    count
  };
}

この Actor は init 関数が一度だけ呼ばれることを想定していますが、強制はしていません。 この init 関数では、Subscriber Actor は自分自身への参照を actor { notify : () → () }; 型で渡します(ここでは上の Subscriber を呼んでいます)。

もし複数回呼ばれた場合、Actor は自分自身を複数回購読することになり、出版者から複数の(重複した)通知を受信することになります。 この脆弱性は、上で示した基本的な出版-購読型パターンの設計の結果です。 より注意深く設計された、より高度な出版者であれば、例えば購読者の重複をチェックして無視することでしょう。

Actor 間の関数の共有

Motoko では、shared Actor 関数はメッセージで他の Actor に送ることができ、後で自分自身や他の Actor から呼び出すことができます。

上に示したコードは説明のために単純化されています。 完全なコードでは出版者と購読者の関係に対してさらなる機能が提供されており、この関係をより柔軟なものにするために shared 関数が用いられています。

例えば、上のコードでは通知用の関数は 常に notify と名付けられています。 より柔軟な設計としては、notify の型だけを固定しておき、購読者 Actor そのものを渡す代わりに subscribe メッセージで指定する shared 関数を購読者が選択できるようにすることが考えられます。

詳しくは、完全なコード例 をご覧ください。

特に、購読者がそのインターフェースの特定の命名規則に縛られることを避けたいとします。 本当に重要なのは、購読者が選んだ ある 関数を発行者が呼び出せるかどうかです。

shared キーワード

この柔軟性を実現するために、Actor は単なる自分自身への参照ではなく、他の Actor からのリモート呼び出しを可能にする一つの 関数 を共有する必要があります。

関数を共有するには、あらかじめ shared と指定する必要があり、型システムはこれらの関数の引数の型、返り値の型、クロージャが包む(close over)データ型が、特定のルールに従うことを強制しています。

Motoko では、public な Actor メソッドに対して shared キーワードを省略することができます。なぜなら、暗黙のうちに(明示的にマークされているかどうかにかかわらず)Actor のパブリック関数は shared でなければならない からです。

shared 関数型を使用すると、上記の例をより柔軟に拡張することができます。 例えば、以下のようにします。

type SubscribeMessage = { callback: shared () -> (); };

これは元の Subscribe 型とは異なり、callback という単一のフィールドを持つ メッセージ のレコード型を記述しており、最初に示したオリジナルの型は notify という単一のメソッドを持つ Actor 型を記述しています。

type Subscriber = actor { notify : () -> () };

注目すべきなのは、actor キーワードが意味するのは、この型はフィールドを持つ通常のレコードではなく、少なくとも 1 つのメソッドがあり、そのメソッドは notify という名前で なければならない ということです。

代わりに SubscribeMessage 型を使用することで、Subscriber Actor は notify メソッドに別の名前を指定することができます。

actor Subscriber {
  var count: Nat = 0;
  public func init() {
    Publisher.subscribe({callback = incr;});
  };
  public func incr() {
    count += 1;
  };
  public query func get(): async Nat {
    count
  };
};

元のバージョンと比較すると、唯一変わっている行は notify の名前を incr に変更し、{callback = incr} 式を用いて新しい subscribe メッセージのペイロードを形成している部分です。

同様に、出版者も対応するインターフェイスを持つように更新することが出来ます。

import Array "mo:base/Array";
actor Publisher {
  var subs: [SubscribeMessage] = [];
  public func subscribe(sub: SubscribeMessage) {
    subs := Array.append<SubscribeMessage>(subs, [sub]);
  };
  public func publish() {
    for (sub in subs.vals()) {
      sub.callback();
    };
  };
};