mcpField
M
motoko

motoko

github.com/dfinity開発39162 回インストール今日更新
リポジトリ

motoko について

Motoko の言語規約、アーキテクチャ指針、実例をまとめたコーディングスキルです。永続化、コレクション、安全な API 設計に関する詳細なルールを、Motoko 開発者向けに提供します。

主な機能

  1. 01Motoko のスタイルとコンパイラー指針
  2. 02複数ファイルのアクター構成
  3. 03永続化と移行に関する注意点
  4. 04mo:core のコレクションパターン
  5. 05更新関数での呼び出し元検証

活用シーン

  1. 01複数ファイルの Motoko プロジェクトの開始
  2. 02永続状態を持つアクターの更新
  3. 03コレクションを使ったデータモデルの実装
  4. 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, removeO(log n)
List可変長配列add, get, atO(1) access
QueueFIFO 処理pushBack, popFrontO(1)
StackLIFO 処理push, popO(1)
Array固定コレクションindex, map, filterO(1) access
Set重複のない値contains, addO(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 ';'

注意点

  1. アクター本体の前に置く型宣言/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
    };
    
  2. バリアントタグの引数は必ず括弧で囲んでください。#tag(x) と記述し、#tag x とは書かないでください。括弧がないと、#tag 1 + 2 は #tag(1) + 2 として解析されます。

  3. Text.join の引数の順序 — 最初がイテレーター、2 番目が区切り文字です。

    Text.join(["a", "b", "c"].vals(), ", ")  // "a, b, c"
    
  4. 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 NatInt.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 typeList<T> ではなく [T] を返し、var フィールドを使用しません
send capability required<system> capability を追加します
field compare does not exist on TimeInt.compare を使用します
unexpected token ';' in function call) の前にある ; を削除します
unbound variable Ximport X "mo:core/X"
M0098 no best choice for type paramlist.map<In, Out>(...)
M0096 on contains callbackfind(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

ベストプラクティス

  1. 常に mo:core を使用し、mo:base は使用しません
  2. stable キーワードは使用しません。拡張直交永続性が状態を処理します
  3. すべての self パラメーター付き関数でドット記法を使用します
  4. ラムダ引数の型を注釈しません。コンパイラーに推論させます
  5. 暗黙の引数を明示的に渡しません
  6. null の場合は switch と Runtime.trap() でアンラップします。不在が想定される場合にのみ ?T を使用します
  7. types.mo / lib/ / mixins/ / main.mo の構成にします
  8. ミックスインには必要な状態の一部のみを渡します
  9. 読み取り専用にはクエリを、状態変更には更新を使用します
  10. 中間コレクションを避けるため、イテレーターをチェーンします
  11. フィールドをコピーする代わりに、レコードスプレッド { self with ... } を使用します

追加の参考資料

  • API ドキュメント: mops.one/core/docs — mo:core の関数シグネチャとドキュメントの正式な情報源です
  • 動作する例: references/examples.md — アクター全体、複数ファイルのアーキテクチャ、タイマー

コメント