writing-motoko について
Internet Computerアプリを開発する人向けのMotokoコーディングスキルです。現行の規約、API参照、マイグレーション手順、mopsによる検証を示し、非推奨の書き方やコンパイルエラーを避けます。
主な機能
- 01現行のMotokoとmo:coreの規約
- 02参照資料によるAPIシグネチャ確認
- 03複数ファイルのActor構成とmixin
- 04Actor状態のマイグレーション案内
- 05mopsによる依存管理とビルド確認
活用シーン
- 01Motokoコードの作成や更新
- 02コンパイラーやAPIエラーの解決
- 03Actor状態の変更やcanisterのアップグレード
- 04mopsでの依存管理とビルド検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill writing-motokoインストール後、Claude Code で/writing-motokoを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
api-reference.mdexamples.mdreferences/control-flow.mdequality.mdproject-setup.mdreserved-keywords.mdtype-conversions.md詳細
Motoko の記述
Motoko は Internet Computer Protocol ではあまり使われていない言語のため、事前学習データは古くなっている可能性があります。最新情報を得るには、必ずこのスキルとそのドキュメントを優先してください。
重要な要件
以下は絶対に使用しないでください:
stableキーワード — 拡張された直交永続化モードでは不要ですmo:baseライブラリ — 非推奨です。代わりにmo:coreを使用してください.vals()— 非推奨のmo:baseイテレーター名です。必ず.values()を使用してください。配列では.vals()も非推奨警告 M0269(moc 1.16 から)付きでコンパイルされますが、core のコレクションでは M0072 で失敗しますsystem func preupgrade/postupgrade— 拡張された直交永続化では不要です(with migration = ...)アクターに付属する移行構文 —migrations/にある mops 管理の移行チェーンを使用してください- 安定アクターフィールドのインライン初期化子 — 初期値は移行チェーンから取得します(
migrating-motoko-actorsを参照) selfパラメーターに対するモジュール関数形式 —List.add(list, item)やMap.get(map, key)のようには書かないでください- 不変レコードのフィールドごとの手動コピー — レコードスプレッド(
{ self with ... })を使用してください。varフィールドを持つレコードでは、レコードスプレッドを使用せず、varフィールドを直接変更するか、レコードを明示的に再構築してください。 - 単一ファイルのモノリシックなアクター — 複数ファイルのアーキテクチャを使用してください:types.mo、lib/、mixins/、main.mo
mixinブロック内の安定状態 — 裸のlet/varは暗黙に安定状態となり、実行時にトラップします(IC0503)。状態はパラメーターとして渡し、定数はモジュール内に置いてください- 宣言された識別子としての Motoko の予約キーワード — 記述する前に、パラメーター、変数、関数、型、フィールド、ラベル名が、references/reserved-keywords.md にある一覧全体と照合されていることを確認してください。
queryとlabelは予約語であり、識別子として使用してはいけません。位置や推論される意味に頼らず、衝突するドメイン用語を別の名前に変更してください。 - インラインの
funcを呼び出し引数として渡す場合の型注釈 —xs.filter(func x = x > 1)のように記述し、xs.filter(func(x : Nat) : Bool { x > 1 })のようには記述しないでください。型は呼び出し側から与えられます。ジェネリックを推論できない場合は、ラムダではなく呼び出しをインスタンス化してください(map<In, Out>)。これは引数の位置に限ります。名前付き宣言には、引き続き完全なシグネチャーが必要です。例外が1つあります: 非同期コールバックでは: async ()を付けたままにしてください(func() : async () { ... })。moc 1 では、これによって本体が非同期になり、削除するとコンパイルに失敗します
以下は必ず使用してください:
mo:coreライブラリ バージョン 2.6.0 以降(コンパイラmoc1.11.2 以降)- コンテキストに応じたドット記法 —
list.add(item)、map.get(key) Map/Setを操作する各ファイルで、キー型のモジュールをインポートします。暗黙のcompareは型そのものではなく、インポートしたモジュールから解決されます(Map.Map<Nat, _>の場合はimport Nat "mo:core/Nat")。キーのモジュールをインポートしていないことが、M0230 の通常の原因です。レコードキーまたはバリアントキーには、独自のcompareを持つモジュールが必要です(Implicit Parameters を参照)。- アンラップしてデフォルト値を返す場合やトラップする場合は、両側にスペースを入れた null 合体演算子
??を使います(opt ?? default、opt ?? Runtime.trap(...))。?Tに対する 2 分岐のswitchよりもこちらを優先してください(moc >= 1.7.0が必要です)。 - ループの反復を終了またはスキップするには、通常の
break/continueを使います。他の言語と同様に、for、while、loop内で使用できます。 - 強化された直交永続化(
stableキーワードなしで状態が永続化されます) - 原則に基づく Motoko アーキテクチャ —
types.mo(型)、lib/(ドメインロジック)、mixins/(API エンドポイント)、main.mo(合成ルート、パブリックメソッドは含めません) - API に不明点がある場合のリファレンス:使い慣れていない
mo:coreAPI を使うとき、またはコンパイル診断が API の不一致を示しているときは、api-reference.md で正確なメソッドシグネチャを確認してください。ここに記載されているのは非推奨ではない API のみです。記載されていないシンボルは使用しないでください。API の形状を推測してはいけません。不確かなシンボルを絞り込んで確認する手間には、常に価値があります。手間を省くために確認を怠ると、幻覚による API を出荷することになり、コンパイル修正にさらに多くの時間がかかります。
コンパイルエラーが発生した場合: api-reference.md を再確認し、正確なメソッドシグネチャを確認してください。
アクターの状態構造を変更する、新しい stable フィールドを導入する、またはキャニスターをアップグレードする前に: migrating-motoko-actors を読み込んでください。このガイダンスは mops 管理の移行チェーンを前提としています。変更に移行が必要な場合は、保留中の移行ファイルに記述するか、保留中のものがない場合は src/backend/migrations/ に新しいファイルを作成します。stable 状態を初めて導入する場合は、必ず移行が必要です(インライン初期化子は使えません)。単純な stable 互換アップグレードでは不要です。スキルを参照してください。移行または互換性の診断がソースの内容と一致しない場合、または移行ファイルを書き込めない場合は、troubleshooting-motoko-migrations を読み込んでください。
ツールチェーン(mops)
設定はすべて mops.toml にあります。使い慣れていない mops エラーが発生した場合に限り、https://docs.mops.one/ を参照してください。
依存関係の管理
mops.tomlの依存関係エントリを手動で編集したり、mops.lockに触れたりしないでください。依存関係のメタデータとロックファイルを一貫した状態に保つため、mopsCLI を使用してください。なお、[toolchain]にも CLI があります:mops toolchain use <tool> [version]。[moc] argsはそのままにしてください。コンパイラフラグはプロジェクトのセットアップ時に一度設定するものです。また、多くのプラットフォームはmops.tomlを管理し、フラグも設定してくれるため、コードの作成中に確認したり変更したりしないでください。自分でプロジェクトをセットアップする場合は、references/project-setup.md を参照してください。mops add <pkg>は、公開パッケージをインストールし、バージョンを正確に固定します。特定のバージョンには@x.y.z、GitHub リポジトリには<url>[#ref]、ローカルパッケージには./pathを使用し、開発依存関係には--devを指定してください。mops addで受け付けるパッケージ名は 1 つだけです。複数のパッケージをインストールする場合は、パッケージごとのコマンドを&&で連結してください。複数のmops add呼び出しを並列実行してはいけません。mops.tomlとmops.lockの更新が競合します。mops update [pkg]はパッケージを更新し、バージョンの固定指定を書き換えます。.moファイルをまとめて変更した後は、mops syncでインポートを調整し、不足している依存関係を追加して、使われていない依存関係を削除します。mops.lockを書き換えるのは、mops add、mops update、mops sync、mops installおよび関連するサポート対象の mops コマンドだけです。- ロックまたは整合性に関するエラーが発生した場合は、
mops installを実行してください。mops.tomlがない、古い、または内容に整合性がないmops.lockを再生成します。mops verifyは修復できないファイルハッシュの不一致を報告し、mops cache cleanは検証済みパッケージの再ダウンロードを強制します。mops.lockに対してchmodを実行したり、削除したり、テキスト編集したりしてはいけません。
チェックとビルド
mops install— 依存関係をインストールし、mops.lockを調整します。ファイルがない場合や、mops.tomlと一致しなくなった場合は再生成します。mops check --fix(高速 — 反復作業に使用) — コンパイルエラーを報告し、プロジェクトで有効になっているスタイル警告(ドット記法、冗長な型インスタンス化、冗長な暗黙の引数)を自動修正します。--fixで強制されるかどうかにかかわらず、このスキルのルールに従ってください。終了コード 0 は成功を示します。エラー形式:file:startLine.startCol-endLine.endCol: severity [code], message。チェックに合格するまで修正を繰り返してください。mops build(低速 — 最後に 1 回だけ実行) — コンパイル済みの.wasmと Candid インターフェースファイル.didを生成します。mops check --fixに合格した後の最終検証にのみ使用し、修正ループの中でmops buildを実行してはいけません。.didファイルは生成されるクライアントバインディングの基となるため、手動で編集してはいけません。
mops check --fix に失敗した場合は、まず標準エラー出力を確認してください。moc を直接呼び出してはいけません。.mo ソースを修正して、チェックを再実行してください。
moc 1 と moc 2
[toolchain] の moc では、どちらのメジャーバージョンも指定できます。このスキルの例はすべて両方でコンパイルできます。プロジェクトで指定されているバージョンにかかわらず、次の形式を使用してください。
- 条件式は括弧で囲みます:
if (c)、while (c)、for (x in xs.values())、switch (e)。条件の後に続く分岐を裸の式で書く場合は、間にスペースを入れます:if (c) -1 else 1。 caseパターンは括弧で囲み、すべての case の末尾に;を付けます:case (#tag(p)) { ... };。??の両側にスペースを入れます。- async 関数の本体にはブロックを使い、
= async { ... }の形式は使いません。 persistentやstableキーワードを付けず、裸のactorを使います。
moc 2 では、moc 1 が拒否する短縮形式(括弧なしの条件式やパターン、; のない case、型注釈のない async コールバックなど)が使用できます。そのため、これらの形式は書かないでください。また、moc 1 の警告をエラーに変換します(M0145、M0215、M0222、M0242 など)。すべての警告をエラーとして扱ってください。
最新の Motoko 機能
コンテキストに応じたドット記法
ルール: 関数に self パラメーターがある場合は、必ずドット記法を使用します。
ドット記法は型固有でもあります。値のモジュールが実際に定義している API にのみ適用されます。JavaScript のようなヘルパーがあると推測せず、api-reference.md で確認してください。Motoko に .some(...) と .every(...) はありません。mo:core での名前は .any(...) と .all(...) です。
ドット呼び出しの解決先となるモジュールは、関数を定義しているモジュールです。通常はレシーバーの型のモジュール(map.get、list.add)ですが、異なる場合もあります。arr.values().toList() は配列のモジュールではなく、List(ターゲット)に対して解決されます。必要なインポートがある場合は、レシーバーではなく定義元のモジュールをインポートします。
map.get(key);
list.add(item);
array.filter(func x = x > 0); // CORRECT
Map.get(map, key);
List.add(list, item); // WRONG (M0236)
// Chaining
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);
変換もレシーバー呼び出しですが、toX 関数はモジュール間で分かれています。ソースモジュールで定義されるもの(Nat8.toNat)もあれば、ターゲットモジュールで定義されるもの("42".toNat() と "-5".toInt() は Text ではなく Nat と Int で定義されています)もあります。そのため、Text レシーバーの変換ではターゲットをインポートする必要があります(import Nat "mo:core/Nat"、import Int "mo:core/Int")。インポートしないと moc は M0070/M0072 を報告し、呼び出しはコンパイルできません。
import Principal "mo:core/Principal";
import Nat "mo:core/Nat";
import Text "mo:core/Text";
func conversions(caller : Principal, myNat : Nat) {
ignore caller.toText(); // CORRECT
ignore myNat.toText(); // CORRECT
ignore "42".toNat(); // CORRECT (defined by Nat, imported above)
ignore "hello".concat(" world"); // CORRECT
ignore Principal.toText(caller); // WRONG (M0236)
ignore Nat.toText(myNat); // WRONG (M0236)
};
equal / compare と == の違い。 コレクションは equal と compare を暗黙の引数として受け取るため、自分で定義したレコードやバリアントにはこれらの関数を記述します。== はコンパイラーが生成する構造的等価性で、共有型でのみ使えます。var フィールドが1つでもあるとレコードは共有型ではなくなり、== はコンパイルできなくなります(M0060)。そのため、レコードの比較に == を使わないでください。プリミティブ型や共有フィールドを == で直接比較するのは問題ありません。また、Nat、Int、Float、およびサイズ指定整数型では、これが唯一の方法です。これらの型では self パラメーターなしで equal(x, y) を宣言しているため、myNat.equal(other) は M0070 で失敗します。これらの型の他のレシーバーメソッド(myNat.toText())は問題ありません。
独自のレコードやバリアントには何も自動生成されません。レコードの compare は明示的な関数でなければならず、カスタムバリアントでは equal と compare の両方を明示的に記述する必要があります。references/equality.md を参照してください。
ミックスイン
細かな状態注入が可能な、合成可能なアクターサービスです。各ミックスインは、独立したファイル内のトップレベルの mixin ブロックとして定義します。
module {
public type User = {
principal : Principal;
username : Text;
};
};
import List "mo:core/List";
import Principal "mo:core/Principal";
import Types "../types";
mixin (users : List.List<Types.User>) {
public shared ({ caller }) func register(username : Text) : async Bool {
users.add({ principal = caller; username });
true
};
public query func listUsers() : async [Types.User] {
users.toArray()
};
};
import List "mo:core/List";
import Types "types";
import AuthMixin "mixins/Auth";
actor {
let users : List.List<Types.User>;
include AuthMixin(users);
};
ミックスインのアンチパターン — 次のコードは絶対に生成しないでください:
// WRONG — mixin is NOT a function inside a module; wrapping in module {} is invalid
module {
public func createMixin(state : ...) : actor { ... } { // M0001: unexpected token 'actor'
actor { public func foo() { ... }; };
};
}
// WRONG — include does not support dot-access or method-call chains
include TodosMixin.createMixin(state); // M0001: unexpected token '.'
// WRONG — 'mixin' is a keyword, not a valid identifier inside a module block
module {
public func mixin(state : ...) { ... }; // M0001: unexpected token 'mixin'
}
ルール:
- ミックスインのファイルには、トップレベルに単独の
mixin (params) { ... };ブロックを記述します。module {}の中に入れたり、関数から返したりしてはいけません。 includeには、名前をそのまま記述して引数を続けます:include MixinName(args)。ドットアクセスやメソッドの連鎖は使用しません。
ミックスインでは安定状態を使用しないでください。 mixin 内のトップレベルの let / var はすべて暗黙的に安定になります。許可されるのは transient のみですが、静的な定義(リテラルなど)はモジュールに置くことを優先してください。
ミックスイン間で状態を共有する — パラメーターとして渡します。 2つ以上のミックスイン間で状態を共有するには、その状態をアクターフィールドとして一度だけ宣言し、同じバインディングを各 include に渡します。その状態を受け取るすべてのミックスインは、同じ値を読み書きします。ミックスインは複数のパラメーターを取れるため、共有状態と独自のプライベート状態の両方を受け取れます。
// types.mo: public type GoogleState = { var connection : ?Conn; var config : ?Cfg };
let google : Types.GoogleState; // declared once; initialized in the migration function
let bookings : Map.Map<Nat, Booking>; // BookingsApi's own state
include GoogleApi(google); // gets `google`
include BookingsApi(google, bookings); // gets the SAME `google`, plus its own bookings
各ミックスインに同じバインディングを渡してください。include で新しいレコードを作らないでください。そうするとミックスインごとに別々のコピーが作られ、一方のミックスインによる書き込みが他方に反映されなくなります。
include GoogleApi({ var connection = google.connection }); // WRONG: NEVER DO THIS!
Null 合体演算子(??)
オプションのアンラップ、デフォルト値の指定、またはトラップのみを行う2分岐の switch より、?? を優先してください。moc >= 1.7.0 が必要です。
// Default when absent
let name = optName ?? "anonymous";
// Fail-fast unwrap — null means a bug / missing invariant
let user = users.find(func u = u.id == caller)
?? Runtime.trap("User not found");
// Nested options — chain instead of nested switches
let start = event.start.dateTime ?? event.start.date ?? "";
// RHS is lazy. A block needs `do`; a record literal needs parens:
let n = opt ?? do { let x = 1; x };
let rec = opt ?? ({ x = 0 });
?v の分岐で値を変換する場合、副作用を実行する場合、またはバリアントや複数のケースを照合する場合は、代わりに switch を使用してください。?? が行うのはアンラップまたは代替値の設定のみです。
// Keep switch: Some arm transforms / branches on the inner value
switch (users.get(caller)) {
case (?u) { u.isAdmin };
case null { false };
};
switch (result) {
case (#ok(value)) { value };
case (#err(e)) { Runtime.trap(e) };
};
references/control-flow.md を参照してください。
暗黙のパラメーター
Map と Set の操作では、比較関数を暗黙の引数として受け取ります。Map.empty() 自体は引数を取りません。比較関数は構築時ではなく、必要な操作(add、get、remove、…)で解決されます。
型推論では、キー型に対応するモジュールから compare を探します。そのため、インポートすることで動作します。
import Map "mo:core/Map";
import Nat "mo:core/Nat"; // this import is what supplies Nat.compare
let map = Map.empty<Nat, Text>();
map.add(5, "hello"); // compare resolved from the imported Nat
import Nat がないと、同じコードは失敗します。型はわかっていても、compare を取得するモジュールがないためです。
type error [M0230], Cannot determine implicit argument `compare` of type (Nat, Nat) -> Order
note: Did you mean to import mo:core/Int or mo:core/Nat?
推論できる場合は、比較関数を明示的に渡さないでください。これは M0237 に該当し、mops check --fix で削除されます。
let ages = Map.empty<Text, Nat>(); // Text.compare resolved at add, from the imported Text
ages.add("Alice", 30); // CORRECT
ages.add(Text.compare, "Alice", 30); // WRONG (M0237)
カスタムキー型も同じように機能します。そのモジュールに compare を定義すると、推論されます。
type Point = { x : Nat; y : Nat };
module Point {
public func compare(a : Point, b : Point) : Order.Order { ... };
};
let points = Map.empty<Point, Text>();
points.add({ x = 1; y = 2 }, "A"); // Point.compare inferred
empty() の型インスタンス化は通常のルールに従い、バインディングに型注釈がない場合にのみ必要です。let m : Map.Map<Nat, Text> = Map.empty(); では型が推論され、その箇所で Map.empty<Nat, Text>() とすると M0223 になります。
アーキテクチャパターン
backend/
├── types.mo # Central schema, state definitions
├── lib/ # Domain logic (stateless modules with self pattern)
├── mixins/ # Service layer (stateless, state injected via parameters)
├── types/ # Type definitions for mixins and lib modules
├── migrations/ # Mops-managed migration chain. See migrating-motoko-actors.
│ # Each file is YYYYMMDD_HHMMSS.mo (a UTC timestamp, not a feature name); applied files are FROZEN.
└── main.mo # Composition root (state owner, NO public methods)
インポートパスの規約
パスはインポート元のファイルからの相対パスです。.mo 拡張子や /lib.mo サフィックスは付けません。
// From main.mo
import Types "types";
import AuthMixin "mixins/Auth";
import UserLib "lib/User";
// From lib/*.mo or mixins/*.mo
import Types "../types";
import UserLib "../lib/User";
// Core library — always absolute
import Map "mo:core/Map";
// WRONG — these all cause M0009
import Types "types.mo";
import Types "types/lib.mo";
import Types "backend/types";
マイグレーションファイル(migrations/*.mo)は自己完結している必要があります。インポートできるのは mo:core/... からのみで、../types やプロジェクトのモジュールからはインポートできません。ルールの詳細は migrating-motoko-actors を参照してください。
アクターは最後に置く
インポートと type/let 宣言はアクターの前に置けます。アクターの後には何も置けません。アクターがファイルの結果であるため、その後に宣言を置くと、アクターが () 以外の文になり、M0096(expression of type actor {...} cannot produce expected type ())で失敗します。いずれにしても、共有型は types.mo に置くようにしてください。
インポートの整理
インポートした識別子を使うファイルにのみ、インポートを追加してください。Time.now() は通常、ドメインの lib/*.mo 実装ファイルに置くため、import Time "mo:core/Time"; も main.mo ではなくそのファイルに置きます。ただし、main.mo 自体が Time.now() を呼び出す場合は除きます。大文字で始まる名前空間への呼び出しには、同じファイル内に対応するインポートが必要です。たとえば、mixin が TodosLib.listTodos(...) を呼び出す場合、そのファイルでは TodosLib "../lib/todos" をインポートするか、実際にインポートした別名を使用する必要があります。未使用のインポートに関する警告は失敗として扱ってください。完了する前に、不要になった Debug、Time、ヘルパーモジュールのインポートを削除してください。
クエリ関数
query は公開関数を読み取り専用の呼び出しとして指定します。この関数は高速に実行され、レプリケーションされません。また、実行した状態変更は、呼び出しの完了時にすべて黙って破棄されます。クエリ内の書き込みはコンパイルされ、実行されますが、エラーも警告もなく消えてしまいます。読み取り専用の関数は query func として宣言し、変更を永続化する必要がある関数は、通常の update func(query なし)にしてください。public query func は public shared query func の省略形であり、どちらの形式も同じように ({ caller }) を受け取ります。
public query func getPosts() : async [Types.PostView] { ... }; // read: query
public shared ({ caller }) func addPost(t : Text) : async Nat { ... }; // persists: update
public query func resetAll() : async () { posts.clear() }; // WRONG: compiles, but the clear is discarded
await が許可される場合
通常の query func からは、呼び出し先が query か update かを問わず、ほかの canister 関数を呼び出せません。その中に await someCall() を記述すると、同じ文に対して常に M0038(await の位置が不正)と M0188(送信機能が必要:「query 関数から shared 関数を呼び出すことはできません」)がペアで発生し、失敗します。関数本体で呼び出す必要があるものに応じて、関数の種類を選びます。
| 関数本体で呼び出す必要があるもの | 宣言 | await できるもの |
|---|---|---|
別の canister の query/composite query | public shared composite query func | それらの query 呼び出し先 |
update(shared func)または oneway | 通常の public shared func | その update |
| canister をまたぐ呼び出しがない場合 | public shared query func | なし |
composite query は、ほかの canister の query を await できる読み取り専用関数です。呼び出し先をループし、それぞれを await します。
public shared composite query func sum(counters : [Counter]) : async Nat {
var total = 0;
for (counter in counters.values()) {
total += await counter.peek();
};
total
};
composite query は query と composite query の呼び出し先を呼び出せますが、update やほかの shared 関数は呼び出せません。そこで update を await すると、M0187(「送信機能が必要……呼び出しが許可されるのは query 関数と composite query 関数のみです」)が発生します。関数本体で update を await する必要がある場合、どの種類の query も使用できないため、update func にします。
composite query を開始できるのは、フロントエンドからの呼び出しなど、ingress 呼び出しとしてのみです。update または oneway func から呼び出すと M0186、通常の query func から呼び出すと M0188 が発生します。呼び出しツリー内では合成され、ほかの canister の query/composite query 関数を呼び出せます。
共有型
公開関数が受け取る値と返す値には、共有型(シリアライズ可能な型)のみを使用できます。
- 共有型:
Nat、Int、Text、Bool、Principal、Blob、Float、[T]、?T、レコード、バリアント - 共有型ではないもの:関数、
varフィールド、オブジェクト、Map、Set、List、Queue、Stack
内部状態で可変コンテナを使用する場合は、API 境界用に別の不変な公開型を定義します。
public type PostInternal = { id : Nat; likedBy : Set.Set<Principal> }; // internal
public type Post = { id : Nat; likedBy : [Principal] }; // shared
public func toPublic(self : Types.PostInternal) : Types.Post {
{ self with likedBy = self.likedBy.toArray() };
};
コレクション
API シグネチャの全容については、api-reference.md を参照してください。
import Map "mo:core/Map";
import List "mo:core/List";
import Queue "mo:core/Queue";
import Stack "mo:core/Stack";
import Array "mo:core/Array";
import Set "mo:core/Set";
Map(B ツリー、O(log n)):Map.empty<K, V>()、.add(k, v)、.get(k) → ?V、.remove(k)、.entries()
List(可変長配列、アクセスは O(1)):List.empty<T>()、.add(item)、.get(i) → ?T、.at(i) → T(OOB でトラップ)
Queue(FIFO):Queue.empty<T>()、.pushBack(item)、.popFront() → ?T
Stack(LIFO):Stack.empty<T>()、.push(item)、.pop() → ?T
Array:[var 0, 0, 0](可変)、[1, 2, 3](不変)
Set(B ツリー、O(log n)):Set.empty<T>()、.add(item)、.contains(item)、.remove(item)
警告:retain コールバック内では、list.add() を呼び出さないでください。項目をその場で更新するには、mapInPlace を使用します。
todos.mapInPlace(
func(todo) {
if (todo.id == targetId) { { todo with completed = not todo.completed } } else {
todo;
};
}
);
重要:型宣言では、必ず不透明型エイリアス(List.List<T>、Map.Map<K, V>、Set.Set<T>)を使用してください。内部構造を直接使用してはいけません。また、.filter や .map() は解決されず、M0072 が発生します。
ドメインヘルパーがコアコレクションを受け取る場合、パラメーターの型には具体的な不透明コレクション型(例:todos : List.List<Types.Todo>)を指定し、ヘルパーファイルでそのモジュールをインポートします。コアコレクション値に対して、{ add : (Types.Todo) -> (); toArray : () -> [Types.Todo] } のような構造的メソッドレコード型のパラメーターを使用しないでください。List.List<T> が add、toArray、clear などのフィールドを持つ期待型を生成できないというコンパイラーエラーが出る場合は、ヘルパーのシグネチャが誤っています。シグネチャを List.List<T> に修正し、コレクションはそのまま使用してください。mo:core/Buffer のような架空のモジュールに切り替えたり、List.toArray(todos) や List.add(todos, todo) のようなモジュール関数呼び出しに戻したりしないでください。
配列とコアコレクションの違い
コアコレクション(List.List<T>、Map.Map<K, V>、Set.Set<T>、Queue.Queue<T>、Stack.Stack<T>)には、各モジュールが自己を引数に取る API を定義しているため、レシーバーヘルパーがあります。型が [T] または [var T] の値は配列のスナップショットであり、List.List<T> ではありません。
let snapshot = list.toArray()の後は、ここに示すか API リファレンスで確認した正確なシグネチャを持つ配列操作のみを、レシーバーのドット記法で使用してください:snapshot.filter(pred)、snapshot.map(mapper)、snapshot.sort(comparator)、snapshot.concat([item])。Array.filter<T>(snapshot, pred)やArray.append(snapshot, [item])のようなモジュール関数として呼び出してはいけません。- 値が配列(
[T])であるか、.toArray()/.filter(...)の結果である場合、.map(...)はすでに配列を返します。その配列に対して.toArray()を追加してはいけません。(List.List<T>.map(...)はListを返すため、呼び出し元が配列を期待している場合は引き続き.toArray()が必要です。) - 配列は述語による検索をサポートしています:
.find(predicate) : ?T、.findIndex(predicate) : ?Nat、.any(predicate)、.all(predicate)はすべてmo:core/Arrayにあります(API リファレンスを参照)。JS の表記である.some(...)/.every(...)は存在しません。.any/.allを使用してください。 - 配列には
.contains(element)もあります(equalは暗黙的に使われるため、要素のみを渡してください)。位置が必要な場合は.indexOf(element)を使ってください。?Natを返すため、オプションのまま扱って使用します。また、等価性ではなく述語で所属を判定する場合は.any(pred)を使ってください:
// Overlap between two tag arrays
let matched = leftTags.any(func left = rightTags.any(func right = left == right));
- 検索のためだけにコレクションをコピーしてはいけません。中間配列を作る無駄を避けるため、
templates.toArray().find(func ...)より、元のList.List<T>に対するtemplates.find(func ...)を優先してください。 - 配列スナップショットを反復処理する場合は
.values()を使用してください。新しい Motoko コードで.vals()を書いてはいけません。
CRUD のリストパターン: List にヘルパーを勝手に作らないでください。filterInPlace は存在せず、var フィールドを持つレコードではレコードスプレッドが失敗します。
呼び出し引数として渡すインラインの func には、型注釈を付けません。 呼び出し側ですでにパラメーター型と結果型が決まるため、注釈を付けると同じ内容を繰り返すことになり、コードの変更に伴ってずれが生じます。式の形式 func x = <expr> を使用してください:
todos.find(func todo = todo.id == targetId); // CORRECT
todos.find(func(todo : Types.Todo) : Bool { todo.id == targetId }); // WRONG: annotated
これは引数の位置に限った話です。名前付き宣言には引き続き完全なシグネチャが必要であり、単独で束縛されたラムダには型推論の手がかりがありません。let f = func x = x > 1 は M0103(cannot infer type of variable)で失敗します。
public func toView(t : Types.Todo) : Types.TodoView { ... }; // annotated, as always
読み手に型が明らかでない場合や、ジェネリック型を推論できない場合は、ラムダではなく呼び出し側に記述してください。そのほうが読みやすく、情報源を一つに保てます:
photos.map(func p = { id = p.id; url = p.url.toText() }); // inferred — preferred
photos.map<PhotoInternal, Photo>(func p = { ... }); // when M0098 demands it
コンパイラーが実際に M0098 を報告した場合にのみ <In, Out> を追加してください。型推論がすでに成功している場合に追加すると、M0223(冗長な型インスタンス化)になります。
唯一の例外は、async を返す必要があるコールバックです。この場合、: async () は moc 1 で重要な役割を果たします。これによって本体が非同期になり、型注釈なしで同じことを表す方法はありません(func() = async { ... } も動作しません)。注釈がないと moc 1 は () -> () と推論するため、呼び出しは M0096 で失敗します。moc 2 は非同期コールバックを推論しますが、両方のバージョンでコードをビルドできるよう、注釈は残してください:
Timer.recurringTimer<system>(#seconds(3600), func() : async () { cleanup() });
// Toggle a mutable field by finding the record and mutating the var field.
switch (todos.find(func todo = todo.id == targetId)) {
case (?todo) {
todo.completed := not todo.completed;
?toView(todo);
};
case null { null };
};
// Delete from List by rebuilding from an array snapshot.
var removed = false;
let snapshot = todos.toArray();
todos.clear();
for (todo in snapshot.values()) {
if (todo.id == targetId) {
removed := true;
} else {
todos.add(todo);
};
};
removed;
// To change an immutable field on a record that also has var fields, rebuild it.
let updated : Types.Todo = {
id = todo.id;
text = newText;
var completed = todo.completed;
createdAt = todo.createdAt;
};
let snapshot = todos.toArray();
todos.clear();
for (todo in snapshot.values()) {
if (todo.id == targetId) {
todos.add(updated);
} else {
todos.add(todo);
};
};
レコードが削除されたかどうかを返す削除形式の操作では、配列スナップショットから再構築する際に var removed = false フラグを使うことを推奨します。パラメーター型が List.List<T> のように size() を明示的に提供している場合を除き、todos.size() を呼び出してはいけません。
反復処理とチェーン
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);
let sum = scores.filter(func s = s > 15).foldLeft(0, func(acc, s) = acc + s);
switch (numbers.find(func n = n > 5)) {
case (?found) { /* use */ };
case null {};
};
配列のソート
Array/配列のレシーバーヘルパー(.sort(...) など)は値を返します。これらを単独の逐次実行文として使用してはいけません。Motoko では、() 以外の式を逐次実行することは拒否されます。
let all = todos.toArray();
let sorted = all.sort(func (a, b) =
if (a.createdAt > b.createdAt) { #less }
else if (a.createdAt < b.createdAt) { #greater }
else { #equal }
);
sorted.map(func todo = {
id = todo.id; text = todo.text; completed = todo.completed; createdAt = todo.createdAt
});
// WRONG: `.sort(...)` returns an array, so this is not a valid statement.
all.sort(func (a, b) = Int.compare(b.createdAt, a.createdAt));
all.map(func todo = { ... });
contains と find
contains(element)―List/Setなどに対する等価性チェックです。述語は受け取りません。find(predicate)―List.List<T>と[T]に対する述語検索です。?Tを返します。List.List<T>と[T]はどちらもcontainsを持ちます。等価性ではなく述語でテストする場合は、.any(func x = ...)を使用してください。
numbers.contains(3); // equal inferred from the imported Nat
friends.contains(p); // likewise from Principal — passing Principal.equal here is M0237
todos.find(func todo = todo.id == targetId); // returns ?Todo
// WRONG: friends.contains(func(f) { f == p }) → M0096/M0103
テキスト検索と大文字・小文字の変換
Motoko の Text では、大文字・小文字の変換と部分文字列チェックに、レシーバーに応じたメソッドを使用します。.toLowerCase() や .toLowercase() などの JavaScript の表記は使用しないでください。また、通常の部分文字列検索に Text.contains(...) を呼び出さないでください。
let term = searchTerm.toLower();
textValue.toLower().contains(#text term)
テキストの結合
join はイテレーターをレシーバー、区切り文字を引数として受け取ります。逆にしやすいので注意してください。ドット記法を使用してください。モジュール形式は M0236 違反であり、mops check --fix が書き換えてくれます。
["a", "b"].values().join(", "); // CORRECT → "a, b"
Text.join(["a", "b"].values(), ", "); // WRONG (M0236)
レシーバーは配列ではなくイテレーターです。配列では、まず .values() を呼び出してください。
バリアントタグの引数
バリアントタグの引数は必ず括弧で囲んでください。タグは直後のアトムだけに結び付き、どの演算子よりも優先されるため、引数を括弧で囲まないと、最初の項以降がすべて黙って失われます。
#tag(n + 1) // CORRECT
#tag n + 1 // WRONG: parses as (#tag n) + 1 → M0060, operator is not defined for operand types
型の明示的なインスタンス化
まず型推論に任せてください。型注釈のないラムダの場合、コンパイラーが .map() の型を自動的に別の型として解決するため、通常の呼び出しを記述します。
let photos = internalPhotos.map(
func p = { id = p.id; url = p.url; uploadedBy = p.uploadedBy.toText() }
);
明示的な型パラメーターを追加するのは、コンパイラーが M0098(no best choice for type parameter)を報告した場合のみです。
let photos = internalPhotos.map<PhotoInternal, Photo>(func p = { ... });
型推論がすでに成功しているのに追加すると、それ自体が警告となる M0223(冗長な型のインスタンス化)が発生し、mops check --fix によって削除されます。呼び出しをインスタンス化する代わりにラムダに注釈を付けるのは、常に誤りです。
引数としての関数リテラル
引数として渡す関数本体の後にセミコロンを付けないでください。
list.filter(func(item) { item.id != targetId }) // CORRECT
list.filter(func(item) { item.id != targetId; }) // WRONG: trailing semicolon makes the block return `()`
命令文ブロックをインラインでブールオペランドとして記述しないでください。
// WRONG: parser can treat the block after `or` as an invalid expression shape.
let matches = titleMatches or {
var found = false;
for (tag in tags.values()) {
if (tag == q) { found := true };
};
found
};
// CORRECT: compute the loop result before the final boolean expression.
var tagMatches = false;
for (tag in tags.values()) {
if (tag == q) { tagMatches := true };
};
let matches = titleMatches or tagMatches;
コンパクトな一行の switch であっても、各 switch ケースは次の case の前にセミコロンで区切る必要があります。moc 2 では ; は省略可能ですが、moc 1 では省略するとエラーになります。
switch (pricing) { case (#free) { true }; case (#paid(_)) { false }; } // CORRECT
switch (pricing) { case (#free) { true } case (#paid(_)) { false } } // WRONG: moc 1 syntax error
宣言の終端記号
module、actor、mixin ブロック内のトップレベルおよびネストされた関数宣言は、; で終わる必要があります。関数の後に }; がない場合、次の宣言の近くで構文エラーとして表面化することがよくあります。たとえば、unexpected token 'public' です。
ローカル変数の可変性
:= で再代入する場合を除き、ローカル束縛には let を使用してください。参照先の値が可変であるという理由だけで、ローカル束縛に var を使用しないでください。コレクションに要素を追加するなど、メソッドを通じてオブジェクトを変更しても、束縛自体を var にする必要はありません。後で束縛を再代入する場合を除き、コレクションのビルダーやその他の累積用オブジェクトには let を使用してください。
安全な Nat 算術
コンパイラーが演算時に結果が非負であると証明できない限り、Nat の減算は避けてください。b > a のとき a - b はトラップし、安全性が前の分岐に依存している場合もコンパイラーが警告することがあります。境界チェック、上限付き加算、ループカウンター、または一方の Nat からもう一方の Nat を減算しないヘルパー分岐を優先してください。
Option の処理
アンラップしてデフォルト値を返す場合や、アンラップしてトラップさせる場合は ?? を優先してください。 ?T を取り出すだけの目的で、ネストした switch を記述しないでください。
// Unwrap with trap when null means something is wrong
let user = users.find(func u = u.id == caller)
?? Runtime.trap("User not found");
// Default when absence is fine
let caption = optLabel ?? "(untitled)"; // not `label` — reserved word
// Only return ?T when absence is a normal, expected outcome
public query func findUserByName(name : Text) : async ?User {
users.find(func u = u.name == name);
};
// Keep switch when the Some arm maps / mutates / has side effects
switch (todos.find(func todo = todo.id == targetId)) {
case (?todo) {
todo.completed := not todo.completed;
?toView(todo);
};
case null { null };
};
エラー処理:Result
呼び出し元が対処できる失敗を返すには、mo:core/Result を使用します。Result<Ok, Err> は { #ok : Ok; #err : Err } であるため、Ok と Err 自体が共有されていれば、Candid として API 境界を越えます。Ok の場合、通常は内部レコードではなくビュー型を共有します。
失敗の意味に応じて戻り値の型を選びます:
| 呼び出しが失敗する理由… | 戻り値 |
|---|---|
| 呼び出し元が修正可能な操作を行った | Result<Ok, Err> |
| 対象が存在しないだけで、それが通常の状態である | ?T |
| このコードが担う不変条件が破られている | トラップ(Runtime.trap) |
import Result "mo:core/Result";
public type BookingError = {
#slotTaken : { until : Time.Time };
#notAuthorized;
#unknownRoom : Nat;
};
public shared ({ caller }) func book(roomId : Nat, at : Time.Time) : async Result.Result<Booking, BookingError> {
// ...
};
エラーを Text にすり替えてはいけません。 Result<Booking, Text> では、すべての呼び出し元(フロントエンドを含む)が、「スロットが埋まっている」と「認証されていない」を区別するために文字列を照合しなければなりません。Err はバリアントにし、各失敗に必要なデータはそれぞれのタグ内に入れます。人間向けのメッセージとしてタグ内に含める Text ペイロードは問題ありませんが、判別子として使ってはいけません。
呼び出し元のエラーでトラップしてはいけません。 トラップが発生すると、メッセージ内の未コミットの状態変更はロールバックされ、不透明な拒否としてフロントエンドに届きます。呼び出し元はその結果に応じた分岐ができず、ユーザーにも対処可能なメッセージが表示されません。トラップは「起こり得ないこと」(上記の ?? Runtime.trap(...) を参照)に限って使用します。
ネストせずにチェーンします。 mapOk、mapErr、chain は self を受け取るため、ほかの self パラメーターを持つ API と同様にドット記法で呼び出し、パイプラインをフラットに保ちます。Result.fromOption には self がありません。これは、値がないことを呼び出し元に見えるエラーにする境界で、?T から変換するためのモジュール呼び出しの橋渡しです。
import Result "mo:core/Result";
import Map "mo:core/Map";
import Nat "mo:core/Nat";
actor {
type Room = { id : Nat; name : Text };
type RoomView = { name : Text };
type BookingError = { #unknownRoom : Nat; #notAuthorized };
let rooms : Map.Map<Nat, Room>;
func toView(room : Room) : RoomView = { name = room.name };
func reserve(room : Room) : Result.Result<Room, BookingError> = #ok(room);
public shared ({ caller }) func book(roomId : Nat) : async Result.Result<RoomView, BookingError> {
Result.fromOption(rooms.get(roomId), #unknownRoom(roomId))
.chain(func room = reserve(room))
.mapOk(toView);
};
};
各分岐で異なる処理を行う場合は、#ok / #err に対して switch を使用します。isOk / isErr の後にアンラップする書き方は避けてください。型が保持していたペイロードが失われます。
エラー処理:try / catch
try は reject を捕捉します。reject には、throw、呼び出しに失敗した await / await*(別の canister の呼び出しなど)、または例外を投げるライブラリ呼び出しがあります。呼び出しが例外を投げるかどうか、またそのタイミングは API の一部です。パッケージ独自のスキルを確認してください。trap は捕捉できません。trap はメッセージを終了し、最後のコミットポイント(最後の await)以降に行われた状態変更をロールバックします(await* は、その本体で await が行われた場合にのみコミットします)。呼び出し先自身のメッセージ(別の canister、または async 関数)で発生した trap は別物で、reject として呼び出し元に届きます。async* の呼び出し先は呼び出し元のメッセージ内で実行されるため、その trap は捕捉できません。await も throw もないコードに try は不要です。
reject が状態をロールバックすることはありません。失敗した呼び出しの前に関数が変更した状態は、そのまま変更されています。catch 節で trap を起こしても、それを確実に元に戻す方法にはなりません。呼び出しが中断した時点で、その状態はコミット済みです。変更を元に戻すことが、catch する最も一般的な理由です。
まず呼び出し先の戻り値の型を確認してください。 Result または ?T を返す呼び出し先は、戻り値で失敗を報告し、その失敗について例外は投げません。戻り値を switch で処理してください。try { ignore await f() } catch _ {} は実際の #err を捨て、対応できるものを何も捕捉しません。
伝播した reject ではできない処理を catch 節で行う場合にのみ、catch してください。
| 必要な処理 | catch 節 |
|---|---|
await の前に変更した状態を元に戻す | 予約を削除する、ジョブを失敗としてマークする |
| 呼び出し元が対処できる失敗を返す | バリアントタグを使って return #err(...) する |
| 復旧する | 再試行する、または別のソースにフォールバックする |
| 1 回の失敗を超えてループやバックグラウンドジョブを継続する | 項目をスキップして続行する |
それ以外の場合は、reject をそのまま伝播させてください。空の catch _ {} は、何も依存していない副作用の周囲にのみ使用します。一部のエラーだけを処理する catch 節では、残りを throw e で再スローします。そこで trap が発生すると、呼び出し元はどれも捕捉できなくなります。
import Error "mo:core/Error";
import Map "mo:core/Map";
import Nat "mo:core/Nat";
import Result "mo:core/Result";
actor {
type Quote = { price : Nat };
type HoldError = { #taken; #quoteFailed : Text };
let holds : Map.Map<Nat, Principal>;
// Stands in for any call that can reject.
func fetchQuote(seat : Nat) : async* Quote {
if (seat == 0) throw Error.reject("HTTP 503");
{ price = 100 };
};
public shared ({ caller }) func hold(seat : Nat) : async Result.Result<Quote, HoldError> {
if (holds.containsKey(seat)) return #err(#taken);
holds.add(seat, caller);
try {
#ok(await* fetchQuote(seat));
} catch e {
// A reject keeps the hold, so release the seat here.
holds.remove(seat);
#err(#quoteFailed(e.message()));
};
};
};
よくあるパターン
Self パターンを使ったモジュール
// lib/User.mo
module {
public type User = Types.User;
public func new(id : Principal, name : Text) : User {
{ id; var name; var isActive = true };
};
public func ban(self : User) { self.isActive := false };
};
// Usage: user.ban(); -- dot notation!
with を使ったレコードのスプレッド
ルール: 不変レコードにはレコードのスプレッドを使用してください。var フィールドを含むレコード型には、レコードのスプレッドを決して使用しないでください。moc 1 では base has non-aliasable var field(M0179)というエラーで拒否され、moc 2 では var フィールドが新しいセルにコピーされるため、結果を変更しても元のレコードには反映されません。
{ self with newField = "" }; // CORRECT for immutable records
// CORRECT for a record type containing var fields:
let updated : Types.Todo = {
id = todo.id;
text = newText;
var completed = todo.completed;
createdAt = todo.createdAt;
};
{ todo with text = newText }; // WRONG if Todo contains any var field
状態の定義
エンティティ型は types.mo に配置します。状態フィールドは actor の直接のバインディングとして定義し、AppState のラッパーは使用しません。
Stable actor フィールドは**型のみ(初期化子なし)**で宣言します(初期値は移行チェーンから与えられます)。Transient フィールドには通常どおり初期化子を使用します。
// types.mo
module {
public type User = {
id : Principal;
var username : Text;
var isActive : Bool;
};
};
// main.mo
actor {
let users : List.List<Types.User>;
let state : { var nextPostId : Nat };
include AuthMixin(users);
};
ミックスイン用の可変状態
ミックスインと共有する予定の var actor フィールド(例:var nextPostId : Nat)は宣言しないでください。var パラメーターは値渡しされるため、ミックスインによる変更は反映されません。可変値をレコードでラップして渡してください。レコードは参照共有されます。actor ではレコードを型のみで宣言します(let state : { var nextPostId : Nat };)。その初期値(例:{ var nextPostId = 0 })は、すべての stable フィールドと同様に、移行チェーンから与えられます。
actor、ミックスイン、ヘルパーモジュール間で、共有する可変状態レコードのフィールド名を正確に維持してください。actor が let state : { var nextId : Nat } を宣言し、ミックスインが state を受け取る場合、ヘルパーのパラメーターも { var nextId : Nat } を受け取り、state.nextId を更新する必要があります。ヘルパーのシグネチャでフィールド名を val や counter に変更したり、{ var val = state.nextId } のようなラッパーコピーを作成したりしないでください。コピーを変更してもコピー自体にしか反映されず、actor の状態は変わりません。
一時状態と静的/モジュールフィールド
拡張された直交永続化により、アクターまたはミックスイン内のトップレベルのすべての let/var は、デフォルトで安定(アップグレードをまたいで永続化)になります。stable キーワードはありません。バインディングの先頭に transient を付けると、安定ストレージに保存されなくなり、永続化される代わりに起動(再起動)のたびに再初期化されます。キャッシュ、ケイパビリティハンドル、定数など、永続化する必要のないものに使用します。
定数 — Motoko には const がなく、アクターまたはミックスイン内に単独で記述した let X = ... は安定状態になります。右辺が静的な式(名前空間化され、再利用可能で、状態ではない)である固定値はモジュールに置き、それ以外はアクター/ミックスイン内で transient let として保持します。
transient let admin = Principal.fromText("..."); // non-static (a call) — can't be a module `let`
transient let cache = Map.empty<Text, User>(); // derived; rebuilt after each upgrade
静的(モジュールの let フィールドで使用できるもの)=リテラル、バリアントタグ(#x)、オプション(?x)、タプル、不変の配列とレコード、関数値、インポートされた名前/変数名、およびこれらに対する .field 投影です。非静的=関数呼び出し、演算子(+、==、#)、制御フロー(if/switch/ループ)、配列インデックス参照(a[i])です。
数値変換の適切な使い方
非推奨警告は失敗として扱ってください。すべての変換は、変換元の値に対して to と記述します。Module.fromX の呼び出しや、サイズ指定の数値モジュールを経由する連鎖は使用しないでください。変換はレシーバー呼び出し1回で行います。
import Nat "mo:core/Nat";
let average = sum.toFloat() / count.toFloat(); // Nat.toFloat
| 代わりに | 次のように記述 |
|---|---|
Float.fromInt(i) | i.toFloat() |
Float.fromInt64(i) | i.toFloat() |
Nat.fromNat64(n) | n.toNat() |
Int.fromNat(n) | n.toInt() |
Nat64.fromNat(n) | n.toNat64() |
Blob.fromArray(bytes) | bytes.toBlob() |
Iter.fromArray(a) | a.values() |
複数回の変換が必要なら、選んだ関数が適切でない可能性があります。api-reference.md で mo:core の正確なシグネチャを確認してください。このリファレンスには非推奨ではない API のみが記載されています。
セキュリティと認可
すべての公開更新関数では、{caller} の分割束縛を使って呼び出し元を必ず検証してください。認可はバックエンドで実施し、クライアント側のチェックを決して信用しないでください。
インターカナスター間呼び出しにサイクルを付与すると(await (with cycles = ...) <call>)、サイクルは呼び出し先に渡されます。そのため、そのような呼び出しを引き起こせるエンドポイントは、支出権限として扱ってください。呼び出し元を検証し、金額に上限を設け、未認証の経路から到達できないようにしてください。プラットフォームによってはアウトバウンドのサイクルが全面的に禁止されています。該当する場合は、ホスティングプラットフォーム独自のガイダンスに従ってください。
よくあるコンパイルエラーのパターン
| エラーパターン | 原因 | 修正 |
|---|---|---|
field append does not exist | Array.append が削除された | レシーバーの .concat(...) |
field put does not exist | Map.put が改名された | .add() |
field delete is deprecated | Map.delete が改名された | .remove() |
field toLowerCase does not exist | JS Text API の表記 | .toLower() |
field toLowercase does not exist | JS Text API の表記 | .toLower() |
You can use the dot notation ... contains | Text の contains の形式が誤っている | text.toLower().contains(#text term) |
operator may trap for inferred type Nat | Nat の演算がトラップする可能性がある | Nat の減算を避け、境界チェックやループを使う |
Int cannot produce expected type Nat | Int と Nat の型が一致しない | .toNat() |
field fromX is deprecated | fromX 変換は非推奨 | ソース値に対する toX の対になる変換:n.toFloat()、bytes.toBlob() |
syntax error, unexpected token '.' | 括弧が不足している | #text (searchTerm.toLower()) |
syntax error, unexpected token ',' | for 内で括弧が不足している | for ((key, value) in map.entries()) |
Compatibility error [M0170] | 移行処理が不足している | migrating-motoko-actors を読み込む |
M0250 initialized stable field | 安定アクターフィールドに初期化子がある | 型のみを宣言し、値を移行処理の NewActor に移す |
M0254 / M0267 initial actor requires field | 安定フィールドが移行処理で提供されていない | 保留中の移行処理の NewActor に追加する |
M0255 stable signature downgrade | チェーンまたは移行設定が削除された | 復元する — 拡張移行は一方向です。troubleshooting-motoko-migrations を読み込む |
shared function has non-shared parameter/return type | API で可変型を使用している | List<T> ではなく [T] を返し、var フィールドを使わない |
send capability required | 非同期でない箇所での非同期処理(async*/ローカルな非共有ターゲット) | <system> capability を追加する |
| M0038 misplaced await + M0188 send capability (paired) | 通常の query func 内に await <call> がある | composite query func(クエリの場合)または通常の update func(更新の場合)にする |
M0187 send capability in a composite query | composite query func から update を呼び出している/await している | 通常の update func にする |
M0186 composite send capability required | 非 composite func から composite query func を呼び出している | composite query を開始できるのは ingress 呼び出しのみです。フロントエンドから呼び出すか、呼び出し先を通常の query にする |
unexpected token '<name>' (M0274 on moc 2) at an identifier declaration | 識別子に予約語を使用している | コントラクトと呼び出し元の両方で一貫して名前を変更する。references/reserved-keywords.md を参照 |
unexpected token 'public' after a function | 宣言の ; が不足している | 関数宣言の末尾に }; を付ける |
M0219 / M0220 actor should be declared persistent | moc 1 で --default-persistent-actors を指定していない | references/project-setup.md を参照 |
M0217 redundant persistent keyword | アクターはすでに永続的である(moc 2、または moc 1 のフラグ指定時) | persistent を削除する |
M0218 redundant stable keyword | EOP での stable | stable を削除する — 通常の let/var はすでに安定しています |
M0064 misplaced '!' | オプションブロックの外に ! がある | do ? { ... } で囲む |
M0145 does not cover value | 網羅的でない switch または let パターン | 不足しているケースまたは case _ を追加する。let には let ... else { ... } を使う |
M0242 implicit oneway function | 戻り値の型がない public func | : async ()、または oneway にする場合は : () |
M0215 field provided but not expected | 期待される型で除外されるレコードフィールド。通常はタイプミス | フィールド名を修正するか、フィールドを削除する |
M0277 redundant async | = async { ... } 形式の関数本体(moc 2) | 本体をブロックとして記述する:func f() : async T { ... } |
M0273 braces enclose a record literal | ?? の後にブロックがある(moc 2) | opt ?? do { ... } |
M0275 expected a block | 分岐が条件式に直結している(moc 2) | if (c) -1 else 1 — 単独の分岐の前にスペースを入れるか、分岐を波括弧で囲む |
M0272 record field in block position | case または関数本体にレコードリテラルがある | ブロックの戻り値としてネストする:case null { { x = 0 } }; |
M0116 pattern cannot consume / M0050 literal does not have expected type | バリアントタグまたは符号付きリテラルを、判定対象の型で保持できない | タグを修正する。より広い型と照合するには、判定対象に型注釈を付ける:switch (n : Int) |
M0074 / M0081 / M0101 inconsistent types | 共通点のない配列要素または分岐 | 型を一致させるか、: Any と注釈する |
M0060 operator not defined for {#tag : T} | バリアントタグを括弧で囲んでいない | #tag(x) を使い、#tag x は使わない |
M0060 operator not defined, on == | var フィールドを持つレコード(共有されない)に == を使用している | 代わりに equal 関数を使う |
M0230 cannot determine implicit argument compare | キー型のモジュールがスコープ内にない | 通常はインポートの不足です。そのファイルでキー型のモジュール(Nat、Text など)を import します。レコード/バリアントのキーの場合は、その型自身のモジュールに compare を追加します |
M0070 expected object type, produces Nat | 数値に対してレシーバーの .equal/.compare を使用している | == または Nat.equal(a, b) を使う |
M0096 actor cannot produce expected type () | アクターの後に宣言がある | アクターはファイル内の最後の宣言でなければなりません |
field compare does not exist on Time | Time.compare がない | Int.compare を使う |
unexpected token ';' in function call | 関数リテラルの後にセミコロンがある | ) の前の ; を削除する |
unbound variable X | インポートが不足している | import X "mo:core/X" |
M0098 no best choice for type param | ジェネリック型に明示的な型指定が必要 | list.map<In, Out>(...) |
M0096 on contains callback | contains に述語を渡している | contains は要素を受け取ります。述語を使う場合、Bool には .any(pred)、要素には .find(pred) を使います |
M0009 import file does not exist | パスが誤っている | 相対パスを使い、.mo 拡張子は付けない |
M0244 variable ... is never reassigned | 不要な var バインディング | := で再代入しない限り、let を使う |
クイックリファレンス
基本型: Nat Int Text Bool Principal ?T [T] [var T] Blob Float — Time.now() は Int(ナノ秒)を返します
一般的な操作: debug_show(value) → Text | assert condition | # "text" による連結 | for、while、loop 内の break / continue
| 構造 | 用途 | 主な操作 | 計算量 |
|---|---|---|---|
| Map | キーと値のペア | get、add、remove | O(log n) |
| List | 拡張可能な配列 | add、get、at | O(1) でアクセス |
| Queue | FIFO 処理 | pushBack、popFront | O(1) |
| Stack | LIFO 処理 | push、pop | O(1) |
| Array | 固定サイズのコレクション | index、map、filter | O(1) でアクセス |
| Set | 一意な値 | contains、add | O(log n) |
ベストプラクティス
- 常に
mo:coreを使用し、mo:baseは使用しないでください stableキーワードは使用しません — 拡張された直交永続化によって状態が処理されます- すべての
selfパラメーター関数にはドット記法を使用してください ??でアンラップします(opt ?? Runtime.trap(...)またはopt ?? default)。switchは変換/副作用/バリアントに使用し、?Tは値が存在しないことが想定される場合にのみ使用してください- types.mo / lib/ / mixins/ / main.mo の構成にします
- ミックスインには、必要な状態のスライスのみを渡します
- 読み取り専用にはクエリを、状態変更にはアップデートを使用してください
- 中間コレクションを避けるため、イテレーターをチェーンします
- 不変レコードにはレコードスプレッド
{ self with ... }を使用します。varフィールドを含むレコードは変更するか、再構築してください - 安定したアクターフィールドにインライン初期化子を指定しないでください — 初期値はマイグレーションチェーンから取得します
- インラインの
func引数には型注釈を付けません(非同期コールバックの: async ()を除く)。代わりに呼び出しをインスタンス化し、コンパイラーが M0098 を報告した場合にのみ行ってください - 拒否される可能性がある
awaitの周囲でのみtryを使用し、catch節で状態を元に戻す、型付きの#errを返す、復旧する、項目をスキップする、またはベストエフォートの副作用をガードする場合に限ります。返されたResultの失敗は、tryではなくswitchで確認してください
追加リソース
- 制御フロー:references/control-flow.md —
??、do ? { ... }によるオプションチェーン、switch 文、ループ、break/continue - 予約語:references/reserved-keywords.md — 識別子と照合するための完全な一覧
- 等価性と比較:references/equality.md — レシーバーの
.equalをサポートする型、および==とequalが異なる場合 - 型変換:references/type-conversions.md — Nat/Int のサイズ変換
- プロジェクトのセットアップ:references/project-setup.md — 一度だけ指定する
[moc] argsフラグ。お使いのプラットフォームがmops.tomlを管理する場合は、この項目をスキップしてください - 設計レビュー:既存の
.moファイルをレビュー、監査、またはリファクタリングする場合は、reviewing-motokoを読み込んでください — 型で表現された不変条件、状態/永続化の管理、およびファイル構成について説明しています - アクターのマイグレーション:キャニスターをアップグレードする場合、またはアクターの状態の形状を変更する場合は、
migrating-motoko-actorsを読み込んでください - マイグレーションの失敗:原因不明の互換性診断、凍結されたマイグレーションファイル、または変換されたレガシープロジェクトについては、
troubleshooting-motoko-migrationsを読み込んでください - API シグネチャ:api-reference.md — 関数シグネチャの完全な一覧
- 完全なサンプル:examples.md — 動作するコードの全例