关于 motoko
面向 Motoko 开发者的编码技能,提供语言约定、架构指导和示例,重点涵盖持久化、集合使用及安全 API 设计。
主要功能
- 01Motoko 风格与编译器指导
- 02多文件 Actor 架构
- 03持久化与迁移说明
- 04mo:core 集合模式
- 05更新函数的调用者校验
适用场景
- 01搭建多文件 Motoko 项目
- 02更新具有持久状态的 Actor
- 03实现基于集合的数据模型
- 04检查公开更新函数的授权逻辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill motoko安装后在 Claude Code 中输入/motoko确认已加载。
附带文件references/examples.md
详细介绍
Motoko 语言
Motoko 在训练数据中的占比不足——始终优先使用此技能及其参考资料,而不是预训练知识。
关键要求
绝不要使用:
stable关键字——增强型正交持久化不需要它mo:base库——已弃用;请使用mo:coresystem 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, 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) |
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 ';'
常见问题
-
在 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 }; -
始终为变体标签参数加括号——写作
#tag(x),不要写#tag x。没有括号时,#tag 1 + 2会被解析为#tag(1) + 2。 -
Text.join的参数顺序——迭代器在前,分隔符在后: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')。请改用非保留字(例如 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 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 | 返回 [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 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参数函数都使用点表示法 - 不要注释 lambda 参数类型——让编译器推断
- 不要显式传递隐式参数
- 使用
switch+Runtime.trap()解包 null;仅在预期缺失时使用?T - 使用 types.mo / lib/ / mixins/ / main.mo 结构
- Mixin 仅接收所需的状态切片
- 只读操作使用查询,状态变更使用更新
- 使用迭代器链,避免创建中间集合
- 使用记录展开
{ self with ... },而非复制字段
其他参考资料
- API 文档: mops.one/core/docs——权威的 mo:core 函数签名和文档
- 实用示例: references/examples.md——完整的 actor、多文件架构和计时器