关于 motoko-general-style-guidelines
面向 Motoko 开发者的风格指南,旨在提升源代码的可读性与一致性,涵盖排版、标点、注释和命名约定。
主要功能
- 01运算符空格与换行
- 02使用两空格缩进,不用制表符
- 03定义分组与注释规范
- 04分号与花括号用法
- 05标识符命名约定
适用场景
- 01统一 Motoko 代码格式
- 02检查代码的命名与标点风格
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ research-ag/ motoko-skills --skill motoko-general-style-guidelines安装后在 Claude Code 中输入/motoko-general-style-guidelines确认已加载。
详细介绍
Motoko 通用风格指南
介绍
为提高 Motoko 源代码的可读性和一致性,本风格指南为 Motoko 源代码的格式设置及其他基本约定提供建议。
排版
空格
-
算术运算符两侧加空格,但若要将优先级更高的运算符子表达式在视觉上分组,则不必加空格。
let z = - 2*x + 3*y + 4*(x*x + y*y); -
比较运算符、布尔运算符和赋值运算符两侧加空格。
4 + 5 <= 5 + 4; not (a or b and not c); v := 0; v += 1; -
=两侧加空格。var v = 0; let r = { a = 1; b = 2 }; -
同样,
:两侧加空格。var v : Nat = 0; func foo(x : Nat, y : Nat) : Nat { x + y } func bar((x, y) : (Nat, Nat)) : Nat { x + y } let w = 1 ^ 0xff : Nat16;理由:在声明中,
:的作用相当于定义中的=。此外,类型注解左侧通常可以是任意复杂的表达式或模式。 -
逗号或分号后加空格,前面不加。
let tuple = (1, 2, 3); let record = { a = 1; b = 2; c = 3 }; -
大括号内加空格,简单变体或记录除外。
func f() { 0 }; f({ a = 1; b = 2; c = 3 }); f({a = 1; b = 2}); // 这样也可以 type Vec3D = { x : Float; y : Float; y : Float }; type Order = { #less; #equal; #more }; type Order = {#less; #equal; #more}; // 这样也可以 type Proc = {h : Nat; w : Nat} -> {#ok; #fail}; -
多行展开时,方括号内加空格。
foo( firstArgument, ( longTupleComponent, anotherLongExpression, moreLongExpression ), [ 1, 2, 3, 4, 5, 6, ], { field1 = 4; field2 = 5; field3 = 6; } ); -
语句关键字与其操作数之间加空格。
if (f()) A else B; for (x in xs.values()) { ... }; switch (compare(x, y)) { case (#less) { A }; case (_) { B }; } assert (x < 100); await (async (0)); -
函数或变体标签与其参数元组之间,以及泛型类型参数列表两侧,不加空格。
type Pair<X> = (X, X); type Id = <X>(X) -> X; let ok = #ok(5); func id<X>(x : X) : X { x }; id<Nat>(5); -
如果参数不是元组、带括号的表达式(参见括号)或用作具名参数列表的记录(参见选择类型),则函数与参数之间加空格。
sin 0.0; g [1, 2, 3]; f{arg1 = 0; arg2 = 0};
理由:尤其是 g[1] 会被误解析为索引操作。
-
访问运算符(如
.、?、!)或索引方括号两侧不加空格。foo(bar).baz[5]().boo; foom(?(bam()! + 1));
换行
-
为每行设置固定的右边距,并拆分定义或表达式。许多人认为 80 列仍是合适的限制。
let sum = a + b + 2*c + d + e + f + g + h + i + k + l + m + n + o + p; // Or: let sum = a + b + 2*c + d + e + f + g + h + i + k + l + m + n + o + p;理由:采用这种格式还有许多其他好处,例如:
-
避免代码在窗口右侧被隐藏。
-
避免并排差异比较中出现随机换行。例如,GitHub 或类似的代码审查工具所显示的情况。
-
让代码在纸张、网站或其他媒介上显示得更美观。
-
-
在运算符后换行。
a + b + c + d + f; foo(bar, baz). boo(); -
拆分函数定义或包含较长参数列表的调用时,每个参数单独占一行。
另外,也可以考虑对较长的参数列表使用记录,参见选择类型。
func someFunction( arg1 : FirstType, arg2 : SecondType, anotherArg : Nat, yetAnother : [Type], func : Nat -> Nat, ) : Nat { ... }; someFunction( veryLongArgumentExpression, anotherVeryLongArgumentExpression, 3, aNestedFunctionCall( alsoWithLongArguments, andMoreSuchArguments, ), moreLongishArgument, );理由:这样可以避免阅读代码时漏看参数,也能避免修改某个表达式后需要重新调整换行。
缩进
-
每级缩进应为 2 个空格。
actor A { public func f() { return; } }理由:代码可能会有很多层嵌套。只使用 2 个空格可以避免浪费屏幕空间。
-
缩进不应取决于前几行的词法内容。
尤其是,不要让缩进与前几行中的字符垂直对齐。
let x = someFunction( arg1, arg2, arg3, arg4, arg5); // 请这样做。 let x = someFunction(arg1, arg2, arg3, arg4, arg5); // 或者这样做。 let x = someFunction(arg1, arg2, arg3, arg4, arg5); // 或者这样做。 let x = someFunction( // 或者这样做。 longArg1, longArg2, longArg3, longArg4, longArg5, ); // 反例! let x = someFunction(arg1, arg2, arg3, arg4, arg5); // 不要这样做!理由:垂直对齐有许多问题,例如:
-
浪费大量水平空间。
-
会造成极不一致的缩进层级,掩盖代码结构。
-
修改某一行时可能需要重新对齐,即使编辑器会自动处理,也会使差异比较变得臃肿且难以辨认。
-
在宽度可变的字体中会完全失效。
经验法则:所有缩进都应是 2 的倍数。
-
-
不要使用制表符。
理由:不同工具对制表符的解释差异很大,而且在许多场景中(例如网页和差异比较)制表符可能会丢失或显示不正确。
分组
-
用空行分隔复杂的多行定义。单行定义可以连续排列。
func foo() { // 此函数会执行许多有趣的操作。 // 它的定义跨越多行。 } func boo() { // 这是另一个复杂的函数。 // 它的定义也跨越多行。 } func add(x : Nat, y : Nat) { return x + y }; func mul(x : Nat, y : Nat) { return x * y }; -
用两个空行分隔不同逻辑组的定义。为每个组添加一行注释作为“节标题”。
// 一个很大的类 class MuffleMiff(n : Nat) { // 访问器 public func miffMuff() : Text { ... } public func sniffMiff() : Nat { ... } // 修改器 public func clearMurk() { ... } public func addMuff(name : Text) { ... } // 处理 public func murkMuffle(param : List<Gnobble>) { ... } public func transformSneezler() { ... } // 内部状态 var miffCount = 0; var mabbleMap = Map<Nat, Text>(); }
注释
-
使用行注释(
//…)。仅在行中间注释或开发期间注释掉部分代码时,才使用块注释(/* … */)。// 以下函数会对给定的 snibble 执行当前的 // pallaboom。如果可以,它会返回 // 合适的 plexus。 func paBoom(s : Snibble) : Handle<Plexus> { let puglet = initPugs(s.crick, 0 /* size */, #local); /* 暂时不要执行那些奇怪的操作…… ... ... */ return polyfillNexus(puglet); // 暂时如此 }原因:行注释便于插入、删除或交换单独的行。
-
在行尾添加解释单行代码的简短注释,并与代码至少间隔两个空格。
paBoom(getSnibble())); // 创建新的 snibble -
多行注释放在代码行之前,并与所描述的代码使用相同的缩进。
func f() { // 尝试使用上一个 snibble 调用当前的 pallaboom。 // 如果成功,我们就得到了新的 plexus; // 如果失败,则报告错误。 let plexusHandle = paBoom(getSnibble())); } -
单独成行的注释首字母大写。句子末尾使用规范的句号。
标点
分号
-
在代码块中,无论前面的声明是否以右花括号
}结尾,Motoko 都一律要求使用分号分隔表达式或局部声明。原因:这与其他类 C 语言不同;这些语言往往采用相当随意的规则。
-
在代码块的最后一个表达式后添加分号,除非整个代码块写在同一行。
类型定义也同样如此。
// 同一行的右花括号前不需要 ; type Vec3D = {x : Float; y : Float; z : Float}; type Result<A> = {#ok : A; #error : Text}; func add(x : Nat, y : Nat) : Nat { return x + y }; // 最后一个分支以 ; 结尾 type Address = { first : Text; last : Text; street : Text; nr : Nat; zip : Nat; city : Text; }; type Expr = { #const : Float; #add : (Expr, Expr); #mul : (Expr, Expr); }; func eval(e : Expr) : Float { switch (e) { case (#const(x)) { x }; case (#add(e1, e2)) { eval(e1) + eval(e2) }; case (#mul(e1, e2)) { eval(e1) * eval(e2) }; }; }原因:统一在行尾添加分号,可以简化行的添加、删除或交换。
花括号
-
函数体、
if或case分支以及循环体都要加上花括号,除非它们作为表达式嵌套出现,且只包含单个表达式。func f(x) { f1(x); f2(x) }; let abs = if (v >= 0) v else -v; let val = switch (f()) { case (#ok(x)) x; case (_) 0 }; func succ(x : Nat) : Nat = x + 1; -
多行花括号子表达式采用“C 风格”布局。
func f() { return; }; if (cond) { foo(); } else { bar(); }; switch (opt) { case (?x) { f(x); }; case (null) {}; };
圆括号
-
Motoko 支持“省略圆括号”风格,也就是说,在大多数情况下圆括号都是可选的,例如函数参数列表或语句操作数,只要它们括起的表达式本身已经有括号即可。比如元组、对象或数组,或简单的常量或标识符。
type Op = Nat -> Nat; let a2 = Array.map<Nat, Nat>(func x { x + 1 }, a); let y = f x; let z = f {}; let choice = if flag { f1() } else { f2() }; switch opt { case null { tryAgain() }; case _ { proceed() }; }; -
避免过度使用省略圆括号的风格。
尤其是,不要同时在语句中省略圆括号和花括号。
// 反例! let choice = if flag x + y else z; // 不要这样做! switch val { case 0 f(); // 不要这样做! case n n + 1; // 也不要这样做! };理由:同时省略两者会减少代码分组的视觉提示,使代码更难阅读。
-
同样,如果函数还包含类型参数,就不要省略函数参数周围的圆括号。
// 反例! foo<Nat> 0; // 不要这样做! -
如果函数类型只有一个参数且没有类型参数,则省略其参数类型周围的圆括号。
但如果函数或类也有类型参数,则不要省略这些圆括号。
type Inv = Nat -> Nat; type Id = <T>(T) -> T; type Get = <X>(C<X>) -> X; // 反例! type Get = <X>C<X> -> X; // 不要这样做!
其他
-
使用
_对数字进行分组。十进制数每 3 位分组,十六进制数每 4 位分组。
let billion = 1_000_000_000; let pi = 3.141_592_653_589_793_12; let mask : Nat32 = 0xff00_ff0f;
命名
风格
-
类型名称(包括类或类型参数)、模块名称和 actor 名称使用
UpperCamelCase。 -
所有其他名称(包括常量和变体字段)使用
lowerCamelCase。module MoreMuff { type FileSize = Nat; type Weekday = {#monday; #tuesday; #wednesday}; type Pair<X> = (X, X); class Container<X, Y>() { ... }; func getValue<Name>(name : Name) : Pair<Name> { ... }; let zero = 0; let pair = getValue<Text>("opus"); var nifty : Nat = 0; object obj { ... }; actor ServerProxy { ... }; };理由:通常约定,类型等“静态”实体使用大写开头,动态值使用小写开头。模块和 actor 比较静态,并且可以导出类型。对象通常不导出类型,且主要用作动态值。
-
缩写词按普通单词的形式拼写。
type HttpHeader = ...; func getUrl() { ... }; let urlDigest = ...; -
标识符名称不要以下划线
_开头,除非用于说明模式中的变量有意不使用。let (width, _color, name) = rumpler(); ... // _color is not used here func foo(x : Nat, _futureFlag : Bool) { ... };理由:类型检查器可能会对未使用的标识符发出警告。可以在名称前显式添加
_来说明这是有意为之,从而抑制警告。这与将关键字
_用作模式通配符的用法一致。
命名约定
-
返回值的函数名称应描述该值。
避免使用多余的
get前缀。dict.size(); list.first(); sum(array); -
执行副作用或复杂操作的函数名称应描述该操作。
dict.clear(); dict.set(key, value); let result = traverse(graph); -
返回
Bool的谓词函数应使用is或has前缀,或使用类似的描述来说明所测试的属性。class Set<X>() { public func size() : Nat { ... }; public func add(x : X) { ... }; public func remove(x : X) { ... }; public func isEmpty() : Bool { ... }; public func contains(x : X) : Bool { ... }; }; -
转换为类型
X或从类型X转换的函数,分别命名为toX和fromX;条件是源类型或目标类型为该函数所属的对象,或该函数所在模块的主要类型。 -
在类或对象中,使用以
_结尾的名称来区分私有变量和 getter。class Cart(length_ : Nat) { var width_ = 0; public func length() : Nat { return length_ }; public func width() : Nat { return width_ }; }原因:在 Motoko 中,函数是一等值,因此函数和其他值标识符共享同一命名空间。
不应使用以下划线
_开头的标识符来表示私有状态,因为这表示该名称未使用(参见 style)。 -
对于全局或公开标识符,以及作用域较大的标识符,使用更长、更具描述性的名称;对于作用域较小的局部标识符,使用较短的名称。
如果没有特别需要说明的内容,使用单字符标识符也可以,尤其是在一致地采用同一命名方案时。
func map(x : Nat, y : Nat) : Nat { x + y }; func eval(e : Expr) : Nat { let n = switch (e) { case (#neg(e1)) { - eval(e1) }; case (#add(e1, e2)) { eval(e1) + eval(e2) }; case (#mul(e1, e2)) { eval(e1) * eval(e2) }; }; Debug.print(n); return n; };原因:与普遍看法相反,过于冗长的局部名称会增加噪声,反而降低可读性。
-
在适当情况下,使用复数形式来描述一组项目,例如列表或数组。
短名称也可以采用这种方式。
func foreach<X>(xs : [X], f : X -> ()) { for (x in xs.values()) { f(x) } }
类型
类型注解
-
对涉及定宽数值类型的定义添加类型注解,以消除重载算术运算符和常量的歧义。
let mask : Nat32 = 0xfc03_ff00; let pivot : Nat32 = (size + 1)/2; let vec : [Int16] = [1, 3, -4, 0];:::note
使用浮点常量可在不添加额外注解的情况下强制使用
Float类型。同样,如果需要,可显式添加+号,将值设为Int类型的正数,而不是Nat。:::
let zero = 1.0; // type Float let offset = +1; // type Int -
同样,对于类型不是
Nat或Int的算术表达式,应添加内联类型注解。if (x & mask == (1 : Nat32)) { ... };:::note
在这类情况下需要为常量添加注解,是 Motoko 类型系统目前的不足之处,我们希望很快能解决。
:::
函数参数无需添加注解,因为其类型通常可从函数推断得出。唯一的例外是参数具有泛型类型且省略了类型实参。
func foo(len : Nat32, vec : [Nat16]) { ... }; func bar<X>(x : X) { ... }; foo(3, [0, 1, 2]); bar<Nat16>(0); bar(0 : Nat16); -
为可变变量添加类型注解,除非其类型显而易见。
var name = "Motoko"; var balance = 0; func f(i : Int) { var j = i; }; var balance : Int = 0; var obj : Class = foo();原因:由于子类型关系,从初始化值推断出的类型未必是预期类型。例如,如果不添加注解,
balance的类型将是Nat,从而无法为其赋整数值。 -
为类中的所有公共字段添加类型注解。
class C(init_ : Nat) { public let init : Nat = init_; public var count : Nat = 0; } -
当函数的返回类型为
()时,省略返回类型注解。func twiceF() { f(); f() }; // 无需写 ": ()" -
函数作为参数传递时,省略其类型注解。
Array.map<Nat, Nat>(func n {n + 1}, a); -
对涉及
Nat或Int以外数值类型的定义添加类型注解,以消除算术运算符和常量之间的重载歧义。let mask : Nat32 = 0xfc03_ff00; let offset : Nat32 = size + 1;
选择类型
-
对于任何不能为负数的整数值,请使用
Nat。 -
仅在存储大量值且空间占用很重要、位操作需要将数字按底层方式解释为位向量,或需要匹配外部要求(例如其他 canister)指定的类型时,才使用固定宽度的
NatN或IntN。 -
避免过多使用选项类型,从而避免过多使用
null。尽可能将它们的使用范围限制到最小。排除
null情况,并尽可能使用非选项类型。 -
如果组件超过 2 或 3 个,请考虑使用记录而非元组。记录只是带有命名字段的简单对象。
请注意,记录类型不必声明,可以直接使用。
func nodeInfo(node : Node) : {parent : Node; left : Node; right : Node} { ... } -
请考虑使用变体而非
Bool来表示二元选择。请注意,变体类型不必声明,可以直接使用。
func capitalization(word : Text) : {#upper; #lower} { ... } -
对于主要用于更改状态或产生其他副作用的函数,尽可能使用返回类型
()。class Set<X>() { public func add(x : X) { ... }; public func remove(x : X) { ... }; ... }; -
对于参数列表较长的情况,请考虑使用记录(仅包含数据的对象)作为参数。
func process({seed : Float; delta : Float; data : [Record]; config : Config}) : Thing { ... }; process{config = Config(); data = read(); delta = 0.01; seed = 1.0};理由:这样可以表达命名参数。调用时可以自由调整参数顺序,也能避免调用者意外地按错误顺序传递参数。
-
高阶函数(例如接受回调参数的函数)应将函数参数放在最后。
理由:这样调用代码更易读;而且在没有柯里化的情况下,把函数放在最前面并无意义,尽管在函数式语言中通常会这样做。
-
不要使用
-1之类的哨兵值来表示无效值。请改用选项类型。
func lookup(x : key) : ?Nat { ... } -
在 Motoko 中,数据默认不可变,除非明确声明为可变。
请谨慎使用可变性类型和定义(
var),仅在必要时使用。理由:可变数据无法在 actor 之间传递或共享。它更容易出错,也更难进行形式化推理,尤其是在涉及并发时。
功能
语句
-
遍历数值范围或容器时,使用
for循环,而不是while循环。for (i in Iter.range(1, 10)) { ... }; for (x in array.values()) { ... };理由:
for循环不容易出错,也更易于阅读。 -
适当时,将
if或switch用作表达式。func abs(i : Int) : Int { if (i < 0) -i else i }; let delta = switch mode { case (#up) +1; case (#dn) -1 }; -
Motoko 要求代码块中的所有表达式都具有
()类型,以避免意外丢弃结果。使用
ignore显式丢弃结果。不需要时,不要使用ignore。ignore async f(); // 触发计算 -
Motoko 允许省略函数末尾的
return,因为代码块的值就是其最后一个表达式的值。当函数较短且采用“函数式”风格时使用这种写法,也就是说,函数不包含复杂的控制流或副作用。
如果函数中包含其他
return语句或命令式控制流,则在末尾显式使用return。func add(i : Nat, j : Nat) : Nat { i + j }; func foo(a : Float, b : Float) : Float { let c = a*a + b*b; c + 2*c*c; }; func gcd(i : Nat, j : Nat) : Nat { if (j == 0) i else gcd(j, i % j); }; func gcd2(i : Nat, j : Nat) : Nat { var a = i; var b = j; while (b > 0) { let c = a; a := b; b := c % b; }; return a; };
对象和记录
-
将对象用作简单记录(即没有私有状态和方法的数据结构)时,使用简写对象语法
{x1 = e1; … ; xN = eN}。 -
创建单例对象时,使用
object。 -
尽可能将对象限制为记录。
理由:只有记录才能用作消息参数或结果,也才能存储在稳定变量中。创建带方法的对象以及在内存中表示它们的成本也更高。
-
完整对象仅用于封装状态或行为。
类
-
使用
class创建多个形状相同的对象。 -
类名应体现其概念功能,而不是实现方式;只有在需要区分同一概念的多种不同实现时才例外。例如,
OrderedMap与HashMap)。 -
类既是类型定义,也是对象的工厂函数。
除非确实需要这两种作用,否则不要使用类;其他情况下请使用普通类型别名或返回对象的函数。
-
不要过度使用类。
适当时,使用定义普通类型及其函数的模块。类仅用于封装状态或行为。
理由:与简单记录类型搭配独立函数相比,带方法的对象有其缺点(见上文)。
-
如果类的值需要可发送(共享),则该类需要提供一对
share/unshare方法,用于转换为可共享表示形式(例如记录)或从中转换回来。:::note
对于不可变类,将
unshare实现为某种静态函数可能看起来更自然。不过,即使对于不可变类,它也可能依赖构造函数参数(例如排序函数),因此采用Map(compareInt).unshare(x)这样的模式似乎更合适。:::
-
目前,避免为类添加过多方法,因为这会带来较高开销。
将方法限制在足够少的一组规范方法中;对于那些不太重要且可基于这些规范方法实现的方法,应将其作为外层模块中的函数。
-
对于“静态”类或方法,使用模块。
模块
-
使用
module对定义(包括类型)进行分组,并为其创建命名空间。 -
在适用的情况下,根据模块实现或为其提供函数的主要类型或类来命名模块。
-
每个模块仅限于一个主要概念、类型或类,或一组紧密关联的概念、类型或类。