mcpField
M
motoko

motoko

github.com/dfinity开发39162 次安装今天更新
代码仓库

关于 motoko

面向 Motoko 开发者的编码技能,提供语言约定、架构指导和示例,重点涵盖持久化、集合使用及安全 API 设计。

主要功能

  1. 01Motoko 风格与编译器指导
  2. 02多文件 Actor 架构
  3. 03持久化与迁移说明
  4. 04mo:core 集合模式
  5. 05更新函数的调用者校验

适用场景

  1. 01搭建多文件 Motoko 项目
  2. 02更新具有持久状态的 Actor
  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)
  • 逐字段手动复制记录——请使用记录展开({ self with ... })
  • 单文件单体 Actor——请使用多文件架构

始终使用:

  • mo:core 库 2.0.0+
  • 上下文点号表示法——list.add(item)、map.get(key)
  • 增强型正交持久化(状态无需 stable 即可持久化)
  • 有原则的架构——types.mo、lib/、mixins/、main.mo

Actor 升级/迁移:对于内联迁移,请加载 migrating-motoko;对于使用 --enhanced-migration 的多次迁移,请加载 migrating-motoko-enhanced。在 --enhanced-migration 下,Actor 字段不能有初始化器——请将其声明为 var x : T;,并在引入该字段的迁移中设置初始值。此技能中的 Actor 示例使用了初始化器,在使用增强型迁移的项目中需要调整。

编译器标志

此技能的约定要求使用:

--default-persistent-actors         all actors are `persistent`, no `stable` keyword needed

--enhanced-orthogonal-persistence 默认已启用。

未启用 --default-persistent-actors 时,普通的 actor { } 会报错 M0220——请改写为 persistent actor { }。persistent 关键字是过渡性的;未来某个主要版本的 moc 将默认使用持久化 Actor。

启用以下警告以强制遵循此技能的代码风格(默认关闭,可自动修复):

-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——在持久化 Actor 中是多余的;会产生警告 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);
Lambda 参数类型

不要标注 Lambda 参数类型——编译器会推断:

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
Mixin

可组合的 Actor 服务,支持细粒度状态注入。Mixin 参数是不可变绑定——参数语法中不能使用 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 等)——即使通过不可变绑定访问,其内容仍可变。对于标量状态(例如计数器),Mixin 可以根据初始值创建局部 var,但这个 var 仅在 Mixin 内可见,Actor 无法访问。

对于结构化的可变状态,请传入带有 var 字段的记录。一个模块可以同时定义状态类型和 Mixin:

// 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);
记录展开

使用记录展开,避免逐个复制字段:

{ 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 中。状态字段直接绑定到 Actor 上——无需包装:

// 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) 访问
QueueFIFO 处理pushBack, popFrontO(1)
StackLIFO 处理push, popO(1)
Array固定集合index, map, filterO(1) 访问
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() } }
);

如果类型参数可以推断出来,则省略它们——不要重复添加。

Option 处理

// 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. 在 actor 主体之前声明类型或 let(M0141)。actor 之前只能出现 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 的参数顺序——迭代器在前,分隔符在后:

    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')。请改用非保留字(例如 myLabel、myFunc,或用 kind 代替 type)。

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 移入 actor 主体
redundant stable keyword (M0218)移除 stable;普通 var 会自动保持稳定
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 type返回 [T] 而非 List<T>,不要使用 var 字段
send capability required添加 <system> capability
field compare does not exist on Time使用 Int.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. 不要注释 lambda 参数类型——让编译器推断
  5. 不要显式传递隐式参数
  6. 使用 switch + Runtime.trap() 解包 null;仅在预期缺失时使用 ?T
  7. 使用 types.mo / lib/ / mixins/ / main.mo 结构
  8. Mixin 仅接收所需的状态切片
  9. 只读操作使用查询,状态变更使用更新
  10. 使用迭代器链,避免创建中间集合
  11. 使用记录展开 { self with ... },而非复制字段

其他参考资料

评论

支持 Markdown