Motokoプログラミング3

2023年2月21日

Motokoプログラミング2の続きである。

先にあげたexamplesリポジトリには多くの例があるのだが、しかし、現行のSDKでは動作しないものが多い。どのようなエラーが発生するかについても記録しておく。

今現在は、とてもじゃないが、これらのエラーを解消して動作するようにはできない。

basic_dao

全く動作しなかった。おそらくは、現在のSDK環境と、このサンプルは互換性が無いものと思われる。

$ dfx deploy
Deploying all canisters.
Creating canisters...
Creating canister basic_dao...
basic_dao canister created with canister id: sp3hj-caaaa-aaaaa-aaajq-cai
Building canisters...
WARN: /home/ysugimura/icp/examples/motoko/basic_dao/src/Types.mo:67.72-67.80: warning [M0154], field hash is deprecated:
For large `Int` values consider using a bespoke hash function that considers all of the argument's bits.

Shrink WASM module size.
Installing canisters...
Creating UI canister on the local network.
The UI canister on the "local" network is "s24we-diaaa-aaaaa-aaaka-cai"
Error: Failed while trying to deploy canisters.
Caused by: Failed while trying to deploy canisters.
  Failed while trying to install all canisters.
    Failed to install wasm module to canister 'basic_dao'.
      Failed to create argument blob.
        Invalid data: Expected arguments but found none.

有名なCancanというアプリも現在の環境では全く動作しない。

random_maze

これもまた動作しない。

Shrink WASM module size.
Building frontend...
Error: Failed while trying to deploy canisters.
Caused by: Failed while trying to deploy canisters.
  Failed to build call canisters.
    Failed while trying to build all canisters.
      The post-build step failed for canister 'su63m-yyaaa-aaaaa-aaala-cai' (random_maze_assets) with an embedded error: Failed to build frontend for network 'local'.: The command '"npm" "run" "build"' failed with exit status 'exit status: 127'.
Stdout:

> random_maze_assets@0.1.0 prebuild
> npm run copy:types


> random_maze_assets@0.1.0 copy:types
> rsync -avr .dfx/$(echo ${DFX_NETWORK:-'**'})/canisters/random_maze/** --exclude='assets/' --exclude='idl/' --exclude='*.wasm' --delete src/declarations

sending incremental file list
created directory src/declarations
index.js
random_maze.did
random_maze.did.d.ts
random_maze.did.js
random_maze.most

sent 2,771 bytes  received 150 bytes  5,842.00 bytes/sec
total size is 2,409  speedup is 0.82

> random_maze_assets@0.1.0 build
> webpack


Stderr:
sh: 1: webpack: not found

threshold-ecdsa

これは動作する。Threshold Edcaとは、しきい値楕円曲線署名アルゴリズムのことだそうだ。

何をしているのかわからないので、とりあえずパス。

superheroes

これも動作しない。

Shrink WASM module size.
Building frontend...
Error: Failed while trying to deploy canisters.
Caused by: Failed while trying to deploy canisters.
  Failed to build call canisters.
    Failed while trying to build all canisters.
      The post-build step failed for canister 'tqtu6-byaaa-aaaaa-aaana-cai' (www) with an embedded error: Failed to build frontend for network 'local'.: The command '"npm" "run" "build"' failed with exit status 'exit status: 127'.
Stdout:

> template_assets@0.0.0 prebuild
> npm run copy:types


> template_assets@0.0.0 copy:types
> rsync -avr .dfx/$(echo ${DFX_NETWORK:-'**'})/canisters/superheroes/** --exclude='assets/' --exclude='idl/' --exclude='*.wasm' --delete src/declarations/

sending incremental file list
index.js
superheroes.did
superheroes.did.d.ts
superheroes.did.js
superheroes.most

sent 4,404 bytes  received 111 bytes  9,030.00 bytes/sec
total size is 4,047  speedup is 0.90

> template_assets@0.0.0 build
> webpack


Stderr:
sh: 1: webpack: not found

pub-sub

動作する。これは、Pub/Subメッセージングモデルのサンプルのようだ。ここで行っているのは単純なことである。

  • 二つのキャニスターpublisher/subscriberを用意する。
  • subscriber(購読者)は、興味のある話題についてpublisherに登録する。自分の対象とする話題であれば、通知してもらうことにる。
  • publisherでは、様々な情報を公開するが、subscriberの興味対象であれば、それに対して通知する。

サンプルの説明では、コマンドラインから動作検証している、CandidのウェブGUIからも可能である。

publisher側

subscriber側

CandidによるウェブUIはpublicなものすべてを表示するので、実際には使わないものもある。

テストの仕方としては以下である。

  • subscriber側のinitに、例えばbananaと入力して呼び出す。これでbananaトピックについて通知される。
  • publishe側のpublisに、bananaと12、bananaと34、orangeと56などを入力して呼び出す。
  • subscriber側でgetCount()すると、46になっている。

注意点としては、subscriberはpublisher側からstableで参照されており、永続化されているので、プログラムを再起動してもカウンタは0にはならない。

このパターンについては、Sharing data and behaviorに説明があるのだが、実際のサンプルプログラムからは大幅に簡略化されているので注意。

次にプログラムのコメントをつけてみる。

publisher側

// Publisher
import List "mo:base/List";

actor Publisher {

  /* 各トピックごとのカウンタ */
  type Counter = {
    topic : Text;
    value : Nat;
  };

  /** 一つの購読者 */
  type Subscriber = {
    // トピック名称
    topic : Text;
    // コールバック関数
    // 別actorのfuncなのでsharedでなければいけない。
    // sharedのfuncは特別な制限が課される。
    callback : shared Counter -> ();
  };

  /* 購読者リスト。初期値としては単にnullが入っているだけ */
  stable var subscribers = List.nil<Subscriber>();

  /* 購読登録 */
  public func subscribe(subscriber : Subscriber) {
    subscribers := List.push(subscriber, subscribers);
  };

  /* トピック名称とカウンタのペアをpublishする
     そのトピックに登録したSubscriberに対してコールバックする
   */
  public func publish(counter : Counter) {
    for (subscriber in List.toArray(subscribers).vals()) {
      if (subscriber.topic == counter.topic) {
        subscriber.callback(counter);
      };
    };
  };
}

subcriber側

// Subscriber

/* publisher側をインポートし、これ以降Publisherという名前で参照てきるようにする */
import Publisher "canister:pub";

actor Subscriber {

  /* カウンタの定義。トピックと値
     Publisher側をインポートしているにも関わらず、もう一度定義が必要なのだろうか?
     この定義はpublisher側と全く同じで重複しているが
     共有する方法があるのか無いのか不明。
     ともれ、構造が同一であれば、あちらとこちらで重複した定義でも
     構わないらしい。
  */
  type Counter = {
    topic : Text;
    value : Nat;
  };

  /* カウンタ値の積算値 */
  var count: Nat = 0;

  /* トピックを指定して初期化すると、
     publisher側にコールバック登録する
   */
  public func init(topic0 : Text) {
    // 別のactorからfuncが呼び出されるためには、
    // そのfuncはsharedでなければいけないが、
    // publicなfuncはすべてsharedになる。
    // sharedなfuncは特別な制限が課される模様
    Publisher.subscribe({
      topic = topic0;
      callback = updateCount;
    });
  };

  /* publisher側からのコールバック
     やってきたCounter値をcountに足し込む
     これは、publisher側の 「Counter -> ()」というfunc定義と同一でなければ
     ならないだろう。
   */  
  public func updateCount(counter : Counter) {
    count += counter.value;
  };

  /* 積算値の値を取得する */
  public query func getCount() : async Nat {
    count;
  };

}

Motokoプログラミング4に続く。