モジュールとインポート

この章では、moduleimport キーワードを使用するさまざまなシナリオの例を紹介します。

これらのキーワードがどのように使われるかを知るために、いくつかのサンプルコードを見てみましょう。

Motoko 標準ライブラリからのインポート

最も一般的なインポートの使用場面の 1 つは、このガイドの例やサンプルリポジトリの Motoko プロジェクトやチュートリアルで見られるように、Motoko 標準ライブラリからモジュールをインポートすることです。 標準ライブラリからモジュールをインポートすることで、同じようなものを最初から書くのではなく、それらのモジュールで定義された値、関数、型を再利用することができます。

以下の 2 行は ArrayResult モジュールから関数をインポートしています。

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

import 宣言には、Motoko モジュールであることを示す mo: という接頭辞が含まれており、宣言には .mo というファイルの拡張子がないことに注意してください。

上記の例では、識別子を使ってモジュールを一括してインポートしていますが、 以下のようなオブジェクトパターンの構文を用いることで、サブセットをモジュールから選択的にインポートすることもできます。

Above example uses an identifier pattern to import modules wholesale, but you can also selectively import a subset of symbols from a module by resorting to the object pattern syntax:

import { map, find, foldLeft = fold } = "mo:base/Array";

この例では、関数 mapfind はそのままインポートされ、関数 foldLeftfold にリネームされます。

ローカルファイルのインポート

Motoko でプログラムを書くためのもう一つの一般的なアプローチは、ソースコードを異なるモジュールに分割することです。 例えば、以下のようなモデルでアプリケーションを設計するとします。

  • ステートを変更する Actor と関数を格納する main.mo ファイル。

  • カスタムの型定義を格納する types.mo ファイル。

  • Actor の外で動作する関数を格納する utils.mo ファイル。

このシナリオでは、3 つのファイルを同じディレクトリに置き、ローカルインポートを使って、必要な場所で関数を利用できるようにすると良いでしょう。

例えば main.mo には、同じディレクトリにあるモジュールを参照するために、以下のような行が含まれています。

import Types "types";
import Utils "utils";

これらの行は Motoko ライブラリではなくローカルプロジェクトのモジュールをインポートしているので、これらのインポート宣言は mo: 接頭辞を使用しません。

この例では、types.moutils.mo のファイルが main.mo ファイルと同じディレクトリに置かれています。 繰り返しになりますが、インポートは .mo 接頭辞を使用しません。

他のパッケージやディレクトリからのインポート

他のパッケージやローカルディレクトリ以外のディレクトリからモジュールをインポートすることもできます。

例えば、以下は依存関係にある redraw パッケージからモジュールをインポートしている例です。

import Render "mo:redraw/Render";
import Mono5x5 "mo:redraw/glyph/Mono5x5";

プロジェクトの依存関係は、パッケージマネージャである Vessel を使うか、プロジェクトの設定ファイルである dfx.json で定義することができます。

この例では、Render モジュールは redraw パッケージのソースコードのデフォルトの場所にあり、Mono5x5 モジュールは redraw パッケージのサブディレクトリである glyph に入っています。

Actor クラスのインポート

モジュールのインポートは、通常ローカルの関数や値のライブラリをインポートするために使用されますが、Actor クラスをインポートするために使用することもできます。 インポートされたファイルが名前付きの Actor クラスで構成されている場合、インポートされたフィールドのクライアントから Actor クラスを含むモジュールを見ることができます。

モジュールには 2 つのコンポーネントがあり、両方とも Actor クラスの名前をとって命名されています。

  • クラスのインタフェースを記述する型定義。

  • クラスのパラメータを引数に取り、非同期でクラスの新しいインスタンスを返す非同期関数。

例えば、Motoko の Actor は Actor と async データ で述べた Counter クラスを次のようにインポートしてインスタンス化することができます。

Counters.mo
actor class Counter(init : Nat) {
  var count = init;

  public func inc() : async () { count += 1 };

  public func read() : async Nat { count };

  public func bump() : async Nat {
    count += 1;
    count;
  };
};
CountToTen.mo
import Counters "Counters";
import Debug "mo:base/Debug";
import Nat "mo:base/Nat";

actor CountToTen {
  public func countToTen() : async () {
    let C : Counters.Counter = await Counters.Counter(1);
    while ((await C.read()) < 10) {
      Debug.print(Nat.toText(await C.read()));
      await C.inc();
    };
  }
};
await CountToTen.countToTen()

Counters.Counter(1) の呼び出しは、ネットワーク上に新しいカウンタをインストールします。インストールは非同期なので、呼び出し元は結果を await しなければなりません。

型注釈 : Counters.Counter はここでは冗長です。Actor クラスの型が必要なときに利用できることを示す目的で記載しています。

他の Canister スマートコントラクトからのインポート

上記の Motoko モジュールをインポートする例に加え、mo: の代わりに canister: という接頭辞を用いることで、Canister スマートコントラクトから Actor とその共有(shared)関数 をインポートすることもできます。

Motoko ライブラリとは異なり、インポートされた Canister は、その Canister スマートコントラクト用の Candid インターフェースを発行する他の Internet Computer 言語(例えばRust)で実装することが可能です。それは Motoko の古いバージョンや新しいバージョンである可能性もあります。

例えば、以下の 3 つの Canister を生成するプロジェクトがあるとします。

  • BigMap(Rust で実装)

  • Connectd(Motoko で実装)

  • LinkedUp(Motoko で実装)

これら3つの Canister はプロジェクトの設定ファイルである dfx.json で宣言され、dfx build を実行することでコンパイルされます。

次に、BigMapConnectd Canister を Motoko LinkedUp Actor 内の Actor として以下のようにインポートすることができます。

import BigMap "canister:BigMap";
import Connectd "canister:connectd";

Canister をインポートするとき、インポートされた Canister の型は Motoko モジュール ではなく Motoko Actor に対応することに注意することが重要です。 この区別は、データ構造がどのように型付けされるかに影響を与えます。

インポートされた Canister の Actor では、型は Motoko からではなく、Canister 用の Candid ファイル(プロジェクト名.did ファイル)から与えられます。

Motoko の Actor 型から Candid のサービス(service)型への変換は、ほとんどの場合には 1 対 1 対応ですが、同じ Candid 型にマッピングするいくつかの異なる Motoko 型が存在します。例えば、Motoko の Nat32Char 型は両方とも Candid の nat32 型としてエクスポートされますが、nat32Char ではなく Motoko の Nat32 として標準的(canonical)にインポートされます。

したがって、インポートされた Canister 関数の型は、それを実装している元の Motoko コードの型と異なる場合があります。 例えば、Motoko の関数の実装で shared Nat32 -> async Char という型があった場合、エクスポートされた Candid 型は (nat32) -> (nat32) になりますが、この Candid 型からインポートされた Motoko の型は実際には(正しいのですがおそらく予想に反する)shared Nat32 -> async Nat32 になります。

インポートモジュールの命名

上記の例のようにインポートモジュールをモジュール名で識別するのが最も一般的な慣習ですが、必ずしもそうする必要はありません。 例えば、名前の衝突を避けるため、あるいは命名を簡単にするために、異なる名前を使用したい場合があります。

標準ライブラリから List モジュールをインポートする際に、架空の collections パッケージの別の List ライブラリと名前が衝突しないように、異なる名前を使用する例を以下に示します。

import List "mo:base/List:";
import Sequence "mo:collections/List";
import L "mo:base/List";