motoko について
Motoko の言語規約、アーキテクチャ指針、実例をまとめたコーディングスキルです。永続化、コレクション、安全な API 設計に関する詳細なルールを、Motoko 開発者向けに提供します。
主な機能
- 01Motoko のスタイルとコンパイラー指針
- 02複数ファイルのアクター構成
- 03永続化と移行に関する注意点
- 04mo:core のコレクションパターン
- 05更新関数での呼び出し元検証
活用シーン
- 01複数ファイルの Motoko プロジェクトの開始
- 02永続状態を持つアクターの更新
- 03コレクションを使ったデータモデルの実装
- 04公開更新関数の認可ロジックの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfinity/ icskills --skill motokoインストール後、Claude Code で/motokoを入力して読み込まれたか確認します。
同梱ファイルreferences/examples.md
詳細
Motoko 言語
Motoko はトレーニングデータに十分含まれていないため、事前学習の知識よりも、常にこのスキルとそのリファレンスを優先してください。
重要な要件
以下は絶対に使用しないでください:
stableキーワード — 拡張直交永続化では不要ですmo:baseライブラリ — 非推奨です。mo:coreを使用してくださいsystem func preupgrade/postupgrade— 拡張直交永続化では不要ですselfパラメーターに対するモジュール関数形式 —List.add(list, item)やMap.get(map, key)のように書かないでください- レコードのフィールドを1つずつ手動でコピーすること — レコードスプレッド(
{ self with ... })を使用してください - 単一ファイルのモノリシックなアクター — 複数ファイルのアーキテクチャを使用してください
以下は必ず使用してください:
- バージョン 2.0.0 以降の
mo:coreライブラリ - コンテキストに応じたドット記法 —
list.add(item)、map.get(key) - 拡張直交永続化(
stableなしで状態が永続化されます) - 原則に基づくアーキテクチャ —
types.mo、lib/、mixins/、main.mo
アクターのアップグレード/移行について: インライン移行には migrating-motoko を、--enhanced-migration を使った複数回の移行には migrating-motoko-enhanced を読み込んでください。--enhanced-migration では、アクターのフィールドに初期化子を指定できません。var x : T; として宣言し、そのフィールドを導入する移行で初期値を設定してください。このスキル内のアクターの例には初期化子が使われているため、enhanced-migration を使うプロジェクトでは調整が必要です。
コンパイラーフラグ
このスキルの規約で必要です:
--default-persistent-actors all actors are `persistent`, no `stable` keyword needed
--enhanced-orthogonal-persistence はデフォルトで有効です。
--default-persistent-actors を指定しない場合、通常の actor { } は M0220 エラーになります。代わりに persistent actor { } と書いてください。persistent キーワードは移行期間中のものです。今後の主要な moc リリースでは、アクターはデフォルトで永続化されるようになります。
このスキルのコーディングスタイルを適用するには、以下の警告を有効にしてください(デフォルトではオフで、自動修正できます):
-W M0236 warn on non-dot-notation calls (suggest contextual dot)
-W M0237 warn on redundant explicit implicit arguments
-W M0223 warn on redundant type instantiation
一時的な状態には transient を使用する
アップグレードのたびにリセットするフィールドには transient を指定してください。対象は、リクエストカウンター、レートリミッター、タイマー ID(タイマーはアップグレード後も維持されません)、一時的なキャッシュ、派生したルックアップテーブルなどです。let と var の両方で使用できます:
actor {
let users = Map.empty<Nat, Text>(); // persists across upgrades
var count : Nat = 0; // persists across upgrades
transient var requestCount : Nat = 0; // resets to 0 on every upgrade
transient var timerId : Nat = 0; // timer must be re-registered after upgrade
transient let cache = Map.empty<Nat, Text>(); // rebuilt on every upgrade
};
永続アクターのフィールドに stable は絶対に指定しないでください。冗長であり、警告 M0218 が発生します。
Motoko の最新機能
コンテキストに応じたドット記法
関数に self パラメーターがある場合は、必ずドット記法を使用してください:
map.get(key);
list.add(item);
array.filter(func x = x > 0);
caller.toText();
myNat.toText();
"hello".concat(" world");
let doubled = numbers.map(func x = x * 2).filter(func x = x > 10);
ラムダ引数の型
ラムダ引数に型を指定しないでください。コンパイラーが推論します:
pairs.map(func(k, v) { k # ": " # v }); // ✓
pairs.map(func((k, v) : (Text, Text)) : Text { // ✗ redundant
k # ": " # v
});
暗黙のパラメーター
比較関数はコンパイラーが自動的に推論します:
let map = Map.empty<Nat, Text>();
map.add(5, "hello"); // Nat.compare inferred
let ages = Map.empty<Text, Nat>();
ages.add("Alice", 30); // Text.compare auto-derived
// Custom types — define compare in a same-named module → auto-inferred
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
コンパイラーが推論できる場合、暗黙の引数を明示的に渡さないでください:
m.add(1, "hello"); // ✓
Map.add(m, Nat.compare, 1, "hello"); // ✗
等値性と比較
== はコンパイラーが生成する構造的等値性を使用します。mo:core の equal/compare は、主に Map、Set、contains などの暗黙の引数として使用されます。
self を使う(ドット呼び出し可能な)モジュールもあります:Text、Principal、Bool、Char、Blob。一方、x, y を使う(ドット呼び出しできない)モジュールもあります:Nat、Int、Float、サイズ付き整数。
s1.equal(s2) // Text.equal has self
Nat.compare(x, y) // Nat.compare does not
ミックスイン
細かな状態注入が可能な、組み合わせ可能なアクターサービスです。ミックスインのパラメーターは不変の束縛です。パラメーター構文では var は使用できません:
mixin (users : List.List<User>) {
public shared ({ caller }) func register(username : Text) : async Bool {
users.add(UserLib.new(caller, username));
true;
};
};
actor {
let users = List.empty<User>();
include AuthMixin(users);
};
変更可能な状態を共有するには、変更可能なコンテナー(List、Map など)を渡してください。不変の束縛を通しても、その内容は変更可能です。カウンターなどのスカラー状態の場合、ミックスインは初期値からローカルの var を作成できますが、その var はミックスイン内に限定され、アクターからは参照できません。
構造化された変更可能な状態を渡すには、var フィールドを持つレコードを使用してください。モジュールでは、状態型とミックスインの両方を定義できます:
// lib/Counter.mo
module {
public type State = { var count : Nat; var name : Text };
public func initState() : State { { var count = 0; var name = "" } };
};
// mixins/Counter.mo
mixin (state : CounterLib.State) {
public func increment() : async Nat { state.count += 1; state.count };
};
// main.mo
let counterState = CounterLib.initState();
include CounterMixin(counterState);
レコードスプレッド
フィールドを1つずつコピーせずに済むよう、レコードスプレッドを使用してください:
{ self with newField = "" }; // ✓
{ id = self.id; text = self.text; completed = self.completed; newField = "" }; // ✗
注意点:レコードスプレッドでは、var フィールドを上書きせずに残すことはできません(M0179)。別の型(例:内部型から公開型)に変換する際、元の型に対象の型にはない var フィールドがある場合は、フィールドを明示的にコピーする必要があります。
アーキテクチャパターン
backend/
├── types.mo # Central schema, state definitions
├── lib/ # Domain logic (stateless modules with self pattern)
├── mixins/ # Service layer (state injected via mixin parameters)
├── migrations/ # Enhanced migration files (--enhanced-migration projects)
│ └── <timestamp>_<Name>.mo
└── main.mo # Composition root (state owner, NO public methods)
エンティティ型は types.mo に記述します。状態フィールドはラッパーを使わず、アクターに直接バインドします:
// types.mo
module {
public type User = { id : Principal; var username : Text; var isActive : Bool };
};
// main.mo
actor {
let users = List.empty<Types.User>();
var nextPostId : Nat = 0;
include AuthMixin(users);
};
インポートパスの規則
パスはインポート元のファイルからの相対パスです。.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";
// 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";
共有型
公開関数が受け取る値と返す値は、共有型(シリアライズ可能な型)のみにしてください。
- 共有型:
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> };
public type Post = { id : Nat; likedBy : [Principal] };
public func toPublic(self : Types.PostInternal) : Types.Post {
{ self with likedBy = Set.toArray(self.likedBy) };
};
コレクション
| 構造 | 用途 | 主な操作 | 計算量 |
|---|---|---|---|
| Map | キーと値のペア | get, add, remove | O(log n) |
| List | 可変長配列 | add, get, at | O(1) access |
| Queue | FIFO 処理 | pushBack, popFront | O(1) |
| Stack | LIFO 処理 | push, pop | O(1) |
| Array | 固定コレクション | index, map, filter | O(1) access |
| Set | 重複のない値 | contains, add | O(log n) |
import Map "mo:core/Map";
import List "mo:core/List";
import Set "mo:core/Set";
インポート要件:型の拡張メソッド(ドット記法)は、対応する mo:core モジュールをインポートした場合にのみ使用できます。たとえば、myArray.find(...) を使うには import Array "mo:core/Array" が必要です。イテレーターのチェーンには import Iter "mo:core/Iter"、myBool.toText() には import Bool "mo:core/Bool" が必要です。エラーメッセージには、不足しているインポートがコンパイラーによって示されます。
警告:retain コールバック内で list.add() を呼び出さないでください。代わりに mapInPlace を使ってください。
型宣言では、常に不透明型エイリアス(List.List<T>、Map.Map<K, V>、Set.Set<T>)を使ってください。
イテレーション
Iter でパイプラインを構築し、最後にのみ実体化してください。中間配列は作成しないでください。
self.values().map(toJson).toArray() // ✓ single allocation
Array.map(List.toArray(self), toJson) // ✗ two allocations
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);
contains と find の違い
contains(element)— 等価性をチェックします。述語は受け取りません。find(predicate)— 述語で検索します。?Tを返します。
numbers.contains(3); // Nat.equal auto-derived
friends.contains(p); // Principal.equal auto-derived
numbers.find(func(n) { n > 3 }); // returns ?Nat
明示的な型のインスタンス化
.map() で異なる型に変換する場合は、型パラメーターを指定してください(指定しない場合は M0098 が発生します)。
let photos = internalPhotos.map<PhotoInternal, Photo>(
func(p) { { id = p.id; url = p.url; uploadedBy = p.uploadedBy.toText() } }
);
型を推論できる場合は、型パラメーターを省略してください。冗長に追加しないでください。
オプションの処理
// Trap on unexpected null
let user = switch (users.find(func(u) { u.id == caller })) {
case (?u) { u };
case (null) { Runtime.trap("User not found") };
};
// Return ?T when absence is normal
public query func findUserByName(name : Text) : async ?User {
users.find(func(u) { u.name == name });
};
Module with 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();
セキュリティ
すべての公開更新関数で、{caller} の分解代入を使って呼び出し元を必ず確認してください。バックエンドで認可を強制してください。
引数としての関数リテラル
引数として渡す関数本体の後にセミコロンを付けないでください。
list.filter(func(item) { item.id != targetId }) // ✓
list.filter(func(item) { item.id != targetId };) // ✗ unexpected token ';'
注意点
-
アクター本体の前に置く型宣言/let 宣言(M0141)。アクターの前に置けるのは
import文のみです。型はtypes.moに移動してインポートすることをおすすめします。// ✗ M0141 — type before actor type UserId = Nat; actor { public query func whois(id : UserId) : async Text { ... }; }; // ✓ recommended — types.mo import Types "types"; actor { public query func whois(id : Types.UserId) : async Text { ... }; // qualify with module name }; -
バリアントタグの引数は必ず括弧で囲んでください。
#tag(x)と記述し、#tag xとは書かないでください。括弧がないと、#tag 1 + 2は#tag(1) + 2として解析されます。 -
Text.joinの引数の順序 — 最初がイテレーター、2 番目が区切り文字です。Text.join(["a", "b", "c"].vals(), ", ") // "a, b, c" -
List.getとList.atの違い:get(n)は?Tを返します(範囲外の場合は null)。at(n)はTを返し、範囲外の場合はトラップします。安全にアクセスするにはgetをおすすめします。
予約語
Motoko の文法で予約されており、識別子として使用できません。使用すると解析エラーが発生します(例:unexpected token 'label')。予約されていない語(type の代わりに myLabel、myFunc、kind など)に変更してください。
actor and assert async await
break case catch class composite
continue debug debug_show do else
false finally flexible for from_candid
func if ignore implicit import
in include label let loop
mixin module not null object
or persistent private public query
return shared stable switch system
throw to_candid transient true try
type var weak while with
async*、await*、await? も予約されていますが、識別子に使えない文字を含むため、識別子と衝突することはありません。
よくあるコンパイルエラーのパターン
| エラーパターン | 修正 |
|---|---|
should be declared persistent (M0220) | --default-persistent-actors を追加するか、persistent actor と記述します |
move these declarations into the body (M0141) | type/let をアクター本体の中に移動します |
redundant stable keyword (M0218) | stable を削除します。通常の var は自動的に stable になります |
field append does not exist | .concat() |
field put does not exist (Map) | .add() |
field delete is deprecated (Map) | .remove() |
Int cannot produce expected type Nat | Int.abs(intValue) |
syntax error, unexpected token '.' | #text (searchTerm.toLower()) |
syntax error, unexpected token ',' | for ((key, value) in map.entries()) |
Compatibility error [M0170] | migrating-motoko または migrating-motoko-enhanced スキルを読み込みます |
shared function has non-shared parameter/return type | List<T> ではなく [T] を返し、var フィールドを使用しません |
send capability required | <system> capability を追加します |
field compare does not exist on Time | 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 | find(pred) != null |
M0009 import file does not exist | 相対パスを使用し、.mo 拡張子は付けません |
M0072 field X does not exist | その型の mo:core モジュールをインポートします |
misplaced '!' (M0064) | do ? { ... } で囲みます |
pattern does not cover value (M0145) | 不足しているケースまたは case _ を追加します |
unexpected token 'X' where X is a keyword | 名前を変更します。X は予約語です(Reserved Keywords を参照) |
制御フロー
// Switch — option unwrapping
let value = switch (map.get(key)) {
case (?v) { v };
case (null) { Runtime.trap("Key not found") };
};
// Switch — variant matching
type Status = { #active; #inactive; #pending : Text };
switch (status) {
case (#active) { "User is active" };
case (#inactive) { "User is inactive" };
case (#pending(reason)) { "Pending: " # reason };
};
// Switch — value matching
switch (statusCode) {
case (200) { "OK" };
case (404) { "Not Found" };
case _ { "Unknown" };
};
// For loops
for ((key, value) in map.entries()) {
Debug.print(key.toText() # ": " # value);
};
for (item in list.values()) {
total += item.score;
};
可能な場合は、命令型ループより .foldLeft() または .map() を優先してください。
ループ内で break と continue を使用します。
for (item in iter) {
if (item.id == targetId) {
result := ?item;
break;
};
};
for (item in list.values()) {
if (not item.isActive) continue;
process(item);
};
クイックリファレンス
基本型: Nat Int Text Bool Principal ?T [T] [var T] Blob Float — Time.now() は Int(ナノ秒)を返します
よく使う操作: debug_show(value) → Text | assert condition | # "text" による連結 | ループ内の break/continue
ベストプラクティス
- 常に
mo:coreを使用し、mo:baseは使用しません stableキーワードは使用しません。拡張直交永続性が状態を処理します- すべての
selfパラメーター付き関数でドット記法を使用します - ラムダ引数の型を注釈しません。コンパイラーに推論させます
- 暗黙の引数を明示的に渡しません
- null の場合は
switchとRuntime.trap()でアンラップします。不在が想定される場合にのみ?Tを使用します - types.mo / lib/ / mixins/ / main.mo の構成にします
- ミックスインには必要な状態の一部のみを渡します
- 読み取り専用にはクエリを、状態変更には更新を使用します
- 中間コレクションを避けるため、イテレーターをチェーンします
- フィールドをコピーする代わりに、レコードスプレッド
{ self with ... }を使用します
追加の参考資料
- API ドキュメント: mops.one/core/docs — mo:core の関数シグネチャとドキュメントの正式な情報源です
- 動作する例: references/examples.md — アクター全体、複数ファイルのアーキテクチャ、タイマー