Actor クラス

Actor クラスは、Actor (Canister スマートコントラクト)のネットワークを プログラム的に 作成することを可能にします。 現在のところ、Actor クラスは別々のソースファイルに定義される必要があります。 Actor クラスの定義とインポートの仕方を説明するため、以下の例では Nat 型のキーからから Text 型の値への Map を実装します。 これは、キーと値の簡単な操作(挿入とルックアップ)のために put(k, v)get(k) の関数を持つ Actor クラスとなります。

この例では、データを分散させるために、キーのセットを n 個のバケットに分割します。ここでは、n = 8に固定します。 キー k のバケット i は、 kn で割った余り、つまり i = k % n で決定されます。 i番目のバケット([0..n]+i+ 番目)は、そのバケットのキーに割り当てられたテキスト値を格納するための専用の Actor を受け取ります。

バケット i に割り当てられる Actor は、サンプルの Buckets.mo ファイルで定義されている Actor クラスである Bucket(i) のインスタンスとして、以下のように取得されます:

Buckets.mo
import Nat "mo:base/Nat";
import Map "mo:base/RBTree";

actor class Bucket(n : Nat, i : Nat) {

  type Key = Nat;
  type Value = Text;

  let map = Map.RBTree<Key, Value>(Nat.compare);

  public func get(k : Key) : async ?Value {
    assert((k % n) == i);
    map.get(k);
  };

  public func put(k : Key, v : Value) : async () {
    assert((k % n) == i);
    map.put(k,v);
  };

};

バケットは、キーと値の現在のマッピングを、可変変数である map に格納します。map は命令的な RedBlack ツリーであり、初期状態では空です。

get(k) 関数では、バケット Actor は k に格納されている任意の値を map.get(k) によって単純に返します。

put(k, v) 関数では、バケット Actor は map.put(k, v) を呼ぶことで、現在の map を、k から ?v にマップする map へとアップデートします。

どちらの関数も、クラス変数である ni を使って、キーがバケットに適しているかどうかを ((k % n) == i) とアサートして検証します。

Map のクライアントは、以下のような調整役の Map Actor と通信することができます:

import Array "mo:base/Array";
import Buckets "Buckets";

actor Map {

  let n = 8; // number of buckets

  type Key = Nat;
  type Value = Text;

  type Bucket = Buckets.Bucket;

  let buckets : [var ?Bucket] = Array.init(n, null);

  public func get(k : Key) : async ?Value {
    switch (buckets[k % n]) {
      case null null;
      case (?bucket) await bucket.get(k);
    };
  };

  public func put(k : Key, v : Value) : async () {
    let i = k % n;
    let bucket = switch (buckets[i]) {
      case null {
        let b = await Buckets.Bucket(n, i); // dynamically install a new Bucket
        buckets[i] := ?b;
        b;
      };
      case (?bucket) bucket;
    };
    await bucket.put(k, v);
  };

};

この例が示すように、Map コードは、Bucket Actor クラスを Buckets モジュールとしてインポートしています。

Actor は n 個の割り当てられたバケットの配列を保持しています。 エントリは最初は null であり、必要に応じて Bucket の Actor が入ります。

get(k, v) 関数では、関数では、Map Actor は:

  • キー kn で割った余りを使って、そのキーに対応するバケットのインデックス i を決定します。

  • i 番目のバケットが存在していなければ、null を返します。

  • i 番目のバケットが存在していれば、bucket.get(k, v) を呼び結果を返します。

put(k, v) 関数では、関数では、Map Actor は:

  • キー kn で割った余りを使って、そのキーに対応するバケットのインデックス i を決定します。

  • バケット i が存在しない場合は、コンストラクタである Buckets.Bucket(i) を非同期に呼び出してバケットを作成し、その結果を待ってから配列 buckets に記録します。

  • bucket.put(k, v) を呼び出し,バケットへ挿入します。

この例ではバケットの数を 8 としていますが、Map Actor を Actor class にして、パラメータ (n : Nat) を追加し、let n = 8; という宣言を省略することで簡単に一般化できます。例えば、以下のようになります:

actor class Map(n : Nat) {

  type Key = Nat
  ...
}

Actor class Map のクライアントは、コンストラクタに引数を渡すことで、ネットワーク内のバケットの(最大)数を自由に決定できるようになりました。