writing-motoko について
Internet Computer Protocol 向けに Motoko を記述する開発者向けのコーディングスキルです。非推奨ではない `mo:core` API、モジュール化されたアクター構成、移行、コンパイラチェックを重視します。
主な機能
- 01最新の Motoko 言語ガイダンス
- 02`mo:core` API とドット記法の規約
- 03複数ファイルのアクターと mixin パターン
- 04アクター状態の移行ガイダンス
- 05mops による依存関係・チェック・ビルド
活用シーン
- 01Motoko の canister 機能の実装
- 02アクターコードのモジュール分割
- 03API とコンパイラエラーの解決
- 04依存関係とアクター状態の更新管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfinity/ icskills --skill writing-motokoインストール後、Claude Code で/writing-motokoを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api-reference.mdcontrol-flow.mdequality.mdexamples.mdproject-setup.mdreserved-keywords.mdtype-conversions.md詳細
Motoko の記述
Motoko は Internet Computer Protocol で十分に扱われていない言語であるため、事前学習データが古くなっている可能性があります。最新情報を得るには、常にこのスキルとそのドキュメントを優先してください。
重要な要件
以下は絶対に使用しないでください:
stableキーワード — 拡張直交永続化モードでは不要ですmo:baseライブラリ — 非推奨です。代わりにmo:coreを使用してください.vals()— 非推奨のmo:baseイテレーター名です。常に.values()を使用してください。配列では.vals()でもコンパイルできるため検出されませんが、core コレクションでは M0072 エラーになりますsystem func preupgrade/postupgrade— 拡張直交永続化では不要です(with migration = ...)アクターに付随する移行構文 —migrations/にある mops 管理の移行チェーンを使用してください- stable アクターフィールドのインライン初期化子 — 初期値は移行チェーンから取得します(
migrating-motoko-actorsを参照) selfパラメーターに対するモジュール関数形式 —List.add(list, item)やMap.get(map, key)のようには書かないでください- 不変レコードのフィールドを手作業で1つずつコピーすること — レコードスプレッド(
{ self with ... })を使用してください。varフィールドを含むレコードでは、レコードスプレッドを使用しないでください。varフィールドを直接変更するか、レコードを明示的に再構築してください - 単一ファイルのモノリシックなアクター — 複数ファイル構成を使用してください:types.mo、lib/、mixins/、main.mo
mixinブロック内の状態 — 単独のlet/varは暗黙に stable となり、実行時にトラップします(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 () { ... })。これにより本体が非同期になり、削除すると M0096 エラーになります
以下は必ず使用してください:
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を備えた独自のモジュールが必要です(「暗黙のパラメーター」を参照)- アンラップしてデフォルト値を返す場合、およびアンラップできないときにトラップする場合は、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 を使う場合の API リファレンス:使い慣れていない
mo:coreAPI を使用する場合や、コンパイル診断で API の不一致が指摘された場合は、api-reference.md で正確なメソッドシグネチャを確認してください。ここに記載されているのは非推奨ではない API のみです。記載されていないシンボルは記述しないでください。API の形を推測してはいけません。不確かなシンボルを対象に調べる手間は、必ずかける価値があります。手間を省くために確認を飛ばすと、幻覚による API を出荷することになり、コンパイル修正にそれ以上のコストがかかります コンパイルエラーが発生した場合: 正確なメソッドシグネチャを確認するため、api-reference.md を再確認してください。
アクターの状態の形状を変更する場合、新しい安定フィールドを追加する場合、またはカニスターをアップグレードする場合: migrating-motoko-actors を読み込んでください。このガイダンスは mops で管理されるマイグレーションチェーン を前提としています。変更にマイグレーションが必要な場合は、src/backend/migrations/ に新しいファイルを作成してください。安定状態を初めて導入するときは、必ずマイグレーションが必要です(インライン初期化子は使用しません)。単純な安定互換アップグレードでは必要ありません。スキルを参照してください。マイグレーションまたは互換性に関する診断結果がソースの記述と一致しない場合や、マイグレーションファイルを作成できない場合は、troubleshooting-motoko-migrations を読み込んでください。
ツールチェーン(mops)
設定はすべて mops.toml にあります。mops.toml の設定、依存関係の管理、および mops check/mops build の詳細については、mops-cli スキルを読み込んでください。
依存関係の管理
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つです。複数のパッケージをインストールする場合は、パッケージを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 が失敗した場合:まず stderr を読んでください。moc を直接呼び出してはいけません。.mo ソースを修正して、チェックを再実行してください。
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; may be a block. Bare record literals need extra braces/parens:
let n = opt ?? { 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 関数を呼び出すことができます。
Shared Types
公開関数が受け取る値と返す値には、shared types(シリアライズ可能な型)だけを使用できます。
- Shared:
Nat、Int、Text、Bool、Principal、Blob、Float、[T]、?T、レコード、バリアント - Not shared:関数、
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() };
};
Collections
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>)には、モジュールが self パラメーターの 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(...)はすでに配列を返します。その配列の map 結果に.toArray()を追加しないでください。(List.List<T>.map(...)はListを返すため、呼び出し元が配列を期待する場合は引き続き.toArray()が必要です。) - 配列は述語検索に対応しています:
.find(predicate) : ?T、.findIndex(predicate) : ?Nat、.any(predicate)、.all(predicate)はすべてmo:core/Arrayにあります(API リファレンスを参照)。JavaScript の表記である.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 パターン: 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 () は重要です。これによって本体が async になり、注釈なしの形式はありません(func() = async { ... } も機能しません)。これがないと、ラムダは () -> () と推論され、呼び出しは M0096 で失敗します:
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 であっても、各 case の間にはセミコロンを置いてください。
switch (pricing) { case (#free) { true }; case (#paid(_)) { false }; } // CORRECT
switch (pricing) { case (#free) { true } case (#paid(_)) { false } } // WRONG
宣言の終端記号
module、actor、mixin ブロック内のトップレベルおよびネストされた関数宣言は、; で終える必要があります。関数の後に }; がない場合、次の宣言付近で構文エラーが発生することがよくあります。たとえば、unexpected token 'public' のようなエラーです。
ローカル変数の可変性
:= で再代入する変数を除き、ローカルバインディングには let を使用してください。参照先の値が可変であるというだけの理由で、ローカルバインディングに var を使用しないでください。コレクションへの追加などのメソッドを通じてオブジェクトを変更する場合、バインディング自体を var にする必要はありません。後からバインディングを再代入するのでなければ、コレクションビルダーやその他のアキュムレーターオブジェクトには let を使用してください。
安全な Nat 演算
演算時点でコンパイラーが結果の非負性を証明できない限り、Nat の減算は避けてください。b > a のとき a - b はトラップし、前の分岐に安全性が依存している場合でも、コンパイラーが警告を出すことがあります。境界チェック、範囲内の加算、ループカウンター、または一方の 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 の後にアンラップする書き方は避けてください。型が保持していたペイロードが失われます。
よく使われるパターン
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 フィールドを含むレコード型にレコードスプレッドを使用してはいけません。Motoko では base has non-aliasable var field というエラーになります。
{ 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 に配置します。状態フィールドはアクターの直接のバインディングとし、AppState ラッパーは使用しません。
安定アクターフィールドは型のみで宣言し、初期化子は付けません(初期値は移行チェーンから取得します)。一時フィールドには通常どおり初期化子を使用します。
// 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 アクターフィールド(例:var nextPostId : Nat)を宣言してはいけません。var パラメーターは値渡しされるため、ミックスインによる変更は元に反映されません。可変値はレコードでラップして渡します。レコードは参照によって共有されます。アクターではレコードを型のみで宣言します(let state : { var nextPostId : Nat };)。その初期値(例:{ var nextPostId = 0 })は、すべての安定フィールドと同様に移行チェーンから取得します。
アクター、ミックスイン、ヘルパーモジュール間で共有する可変状態レコードでは、フィールド名を正確に維持してください。アクターが let state : { var nextId : Nat } と宣言し、ミックスインが state を受け取る場合、ヘルパーのパラメーターも { var nextId : Nat } を受け取り、state.nextId を更新する必要があります。ヘルパーのシグネチャでフィールド名を val や counter に変更したり、{ var val = state.nextId } のようなラッパーのコピーを作成したりしてはいけません。コピーを変更しても、それ自体が変わるだけで、アクターの状態は変わりません。
一時状態と静的フィールド/モジュールフィールド
強化された直交永続化により、アクターまたはミックスイン内のすべてのトップレベルの 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 | stable なアクターフィールドに初期化子が指定されています | 型のみを宣言し、値を移行の NewActor に移します |
M0254 / M0267 initial actor requires field | stable フィールドが移行で提供されていません | 保留中の移行の 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(async*/ローカルな非 shared ターゲット)で 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 を呼び出しています | ingress の呼び出しのみが composite query を開始します。フロントエンドから呼び出すか、呼び出される側を通常の query にします |
unexpected token '<name>' at an identifier declaration | 識別子に予約語が使われています | コントラクトと呼び出し元全体で一貫して名前を変更します。references/reserved-keywords.md を参照してください |
unexpected token 'public' after a function | 宣言の ; が不足しています | 関数宣言の末尾に }; を付けます |
M0219 implicitly transient | アクターが永続化されていません | persistent actor と記述します。references/project-setup.md を参照してください |
M0220 actor should be declared persistent | アクターが永続化されていません | persistent actor と記述します。references/project-setup.md を参照してください |
M0218 redundant stable keyword | EOP で stable が指定されています | stable を削除します — 通常の let/var はすでに stable です |
M0064 misplaced '!' | オプションブロックの外で ! が使われています | do ? { ... } で囲みます |
M0145 does not cover value | switch が網羅的ではありません | 不足しているケースまたは case _ を追加します |
M0060 operator not defined for {#tag : T} | バリアントタグが括弧で囲まれていません | #tag(x) を使い、#tag x は使わないでください |
M0060 operator not defined, on == | (shared ではない)var フィールドを持つレコードに == を使っています | 代わりに equal 関数を使います |
M0230 cannot determine implicit argument compare | キー型のモジュールがスコープにありません | 通常は import の不足です。そのファイルでキー型のモジュール(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 が不足しています | 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 を報告した場合に限ります
参考情報
- 制御フロー: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 — 動作するコードサンプル一式
- mops ツール:
mops.tomlの設定、依存関係の管理、mops check/mops build/ツールチェーンのセットアップについては、mops-cliを読み込みます