motoko-general-style-guidelines

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Motoko general style guidelines

Motoko通用风格指南

What This Is

本指南说明

To increase readability and uniformity of Motoko source code, the style guide provides suggestions for formatting Motoko sources and other basic conventions.
为提升Motoko源代码的可读性与一致性,本风格指南提供了Motoko代码格式规范及其他基础约定建议。

Layout

布局

Spacing

空格

  • Put spaces around arithmetic operators, except to visually group sub-expressions of more tightly binding operators.
    motoko
    let z = - 2*x + 3*y + 4*(x*x + y*y);
  • Put spaces around comparison operators, Boolean operators, and assignment operators.
    motoko
    4 + 5 <= 5 + 4;
    not (a or b and not c);
    v := 0;
    v += 1;
  • Put spaces around '='.
    motoko
    var v = 0;
    let r = { a = 1; b = 2 };
  • Analogously, put spaces around
    :
    .
    motoko
    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;
    Rationale: ':' is to declarations what '=' is to definitions. Moreover, the left-hand of a type annotation may generally be an arbitrary complex expression or pattern.
  • Put a space after a comma or semicolon, but not before.
    motoko
    let tuple = (1, 2, 3);
    let record = { a = 1; b = 2; c = 3 };
  • Put spaces inside braces, unless they are a simple variant or record.
    motoko
    func f() { 0 };
    f({ a = 1; b = 2; c = 3 });
    f({a = 1; b = 2});  // okay as well
    
    type Vec3D = { x : Float; y : Float; y : Float };
    type Order = { #less; #equal; #more };
    
    type Order = {#less; #equal; #more};  // okay as well
    type Proc = {h : Nat; w : Nat} -> {#ok; #fail};
  • Put spaces inside brackets if they stretch multiple lines.
    motoko
    foo(
      firstArgument,
      ( longTupleComponent, anotherLongExpression,
        moreLongExpression
      ),
      [ 1, 2, 3,
        4, 5, 6,
      ],
      { field1 = 4; field2 = 5;
        field3 = 6;
      }
    );
  • Put a space between statement keywords and their operands.
    motoko
    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));
  • Do not put a space between a function or variant tag and its argument tuple or around a generic type parameter list.
    motoko
    type Pair<X> = (X, X);
    type Id = <X>(X) -> X;
    
    let ok = #ok(5);
    
    func id<X>(x : X) : X { x };
    id<Nat>(5);
  • Put a space between a function and its argument if it is not a tuple or parenthesized expression (see parentheses) or a record used as a named parameter list (see picking types).
    motoko
    sin 0.0;
    g [1, 2, 3];
    f{arg1 = 0; arg2 = 0};
Rationale:
g[1]
in particular will be misparsed as an indexing operation.
  • Do not put a space around access operators like
    .
    ,
    ?
    ,
    !
    , or index brackets.
    motoko
    foo(bar).baz[5]().boo;
    foom(?(bam()! + 1));
  • 算术运算符前后需加空格,但为了视觉上区分优先级更高的子表达式时除外。
    motoko
    let z = - 2*x + 3*y + 4*(x*x + y*y);
  • 比较运算符、布尔运算符和赋值运算符前后需加空格。
    motoko
    4 + 5 <= 5 + 4;
    not (a or b and not c);
    v := 0;
    v += 1;
  • =
    前后需加空格。
    motoko
    var v = 0;
    let r = { a = 1; b = 2 };
  • 同理,
    :
    前后需加空格。
    motoko
    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;
    理由:
    :
    在声明中的作用等同于
    =
    在定义中的作用。此外,类型注解的左侧通常可以是任意复杂的表达式或模式。
  • 逗号或分号后需加空格,前侧不加。
    motoko
    let tuple = (1, 2, 3);
    let record = { a = 1; b = 2; c = 3 };
  • 大括号内部需加空格,除非是简单的变体或记录。
    motoko
    func f() { 0 };
    f({ a = 1; b = 2; c = 3 });
    f({a = 1; b = 2});  // okay as well
    
    type Vec3D = { x : Float; y : Float; y : Float };
    type Order = { #less; #equal; #more };
    
    type Order = {#less; #equal; #more};  // okay as well
    type Proc = {h : Nat; w : Nat} -> {#ok; #fail};
  • 如果括号内容跨越多行,括号内部需加空格。
    motoko
    foo(
      firstArgument,
      ( longTupleComponent, anotherLongExpression,
        moreLongExpression
      ),
      [ 1, 2, 3,
        4, 5, 6,
      ],
      { field1 = 4; field2 = 5;
        field3 = 6;
      }
    );
  • 语句关键字与其操作数之间需加空格。
    motoko
    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));
  • 函数或变体标签与其参数元组之间不加空格,泛型类型参数列表前后也不加空格。
    motoko
    type Pair<X> = (X, X);
    type Id = <X>(X) -> X;
    
    let ok = #ok(5);
    
    func id<X>(x : X) : X { x };
    id<Nat>(5);
  • 如果函数的参数不是元组、带括号的表达式(参见括号)或用作命名参数列表的记录(参见类型选择),则函数与参数之间需加空格。
    motoko
    sin 0.0;
    g [1, 2, 3];
    f{arg1 = 0; arg2 = 0};
    理由:尤其是
    g[1]
    会被误解析为索引操作。
  • 访问运算符(如
    .
    ?
    !
    )或索引括号前后不加空格。
    motoko
    foo(bar).baz[5]().boo;
    foom(?(bam()! + 1));

Line breaks

换行

  • Pick a fixed right margin for lines and break definitions or expressions. 80 still is considered a good limit by many.
    motoko
    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;
    Rationale: Among other reasons, this style of formatting:
    1. Avoids code being hidden to the right in a window.
    2. Avoids random line breaks in side-by-side diffs. For example, as shown by GitHub or similar code review tools.
    3. Allows prettier display on paper, web sites, or other media.
  • Break lines after an operator.
    motoko
    a + b + c +
      d + f;
    
    foo(bar, baz).
      boo();
  • When breaking function definitions or calls with long argument lists, put each argument on a separate line.
    Also, consider using records for long parameter lists, see picking types.
    motoko
    func someFunction(
      arg1 : FirstType,
      arg2 : SecondType,
      anotherArg : Nat,
      yetAnother : [Type],
      func : Nat -> Nat,
    ) : Nat {
      ...
    };
    
    someFunction(
      veryLongArgumentExpression,
      anotherVeryLongArgumentExpression,
      3,
      aNestedFunctionCall(
        alsoWithLongArguments,
        andMoreSuchArguments,
      ),
      moreLongishArgument,
    );
    Rationale: This prevents overlooking an argument when reading code and avoids re-breaking lines when changing one of the expressions.
  • 为代码行设置固定的右侧边界,超出时拆分定义或表达式。很多人认为80字符仍是合适的限制。
    motoko
    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;
    理由:这种格式风格有诸多好处,例如:
    1. 避免代码在窗口右侧被隐藏。
    2. 避免在并排对比差异时出现随机换行,例如GitHub或类似代码审查工具显示的差异。
    3. 便于在纸张、网站或其他媒介上美观展示。
  • 在运算符后换行。
    motoko
    a + b + c +
      d + f;
    
    foo(bar, baz).
      boo();
  • 当函数定义或调用的参数列表过长时,将每个参数单独放在一行。
    此外,考虑使用记录来处理长参数列表,参见类型选择
    motoko
    func someFunction(
      arg1 : FirstType,
      arg2 : SecondType,
      anotherArg : Nat,
      yetAnother : [Type],
      func : Nat -> Nat,
    ) : Nat {
      ...
    };
    
    someFunction(
      veryLongArgumentExpression,
      anotherVeryLongArgumentExpression,
      3,
      aNestedFunctionCall(
        alsoWithLongArguments,
        andMoreSuchArguments,
      ),
      moreLongishArgument,
    );
    理由:这样可以避免阅读代码时遗漏参数,同时修改其中一个表达式时无需重新调整换行。

Indentation

缩进

  • Each level of indentation should be 2 spaces.
    motoko
    actor A {
      public func f() {
        return;
      }
    }
    Rationale: There may be a lot of nesting. Using only 2 spaces avoids wasting screen estate.
  • Indentation should not depend on the lexical contents of previous lines.
    In particular, do not vertically align indentation with inner characters from previous lines.
    motoko
    let x = someFunction(
      arg1, arg2, arg3, arg4, arg5);               // Do this.
    
    let x = someFunction(arg1, arg2, arg3,
      arg4, arg5);                                 // Or this.
    
    let x =
      someFunction(arg1, arg2, arg3, arg4, arg5);  // Or this.
    
    let x = someFunction(                          // Or this.
      longArg1,
      longArg2,
      longArg3,
      longArg4,
      longArg5,
    );
    
    // COUNTER EXAMPLE!
    let x = someFunction(arg1, arg2, arg3,
                         arg4, arg5);              // DO NOT DO THIS!
    Rationale: There are many problems with vertical alignment, for example:
    1. It wastes a lot of horizontal space.
    2. It creates wildly inconsistent indentation levels that obfuscate the structure of the code.
    3. It can produce realignment churn when changing a line, which, even when automated by editors, inflates and obfuscates diffs.
    4. It completely breaks with variable-width fonts.
    Rule of thumb: there should be no indentation that is not a multiple of 2.
  • Do not use tabs.
    Rationale: The interpretation of tabs varies wildly across tools and they get lost or are displayed incorrectly in many contexts, such as web pages, diffs, etc.
  • 每级缩进使用2个空格。
    motoko
    actor A {
      public func f() {
        return;
      }
    }
    理由:代码可能存在大量嵌套,仅使用2个空格可避免浪费屏幕空间。
  • 缩进不应依赖于前一行的词汇内容。
    尤其不要将缩进与前一行的内部字符垂直对齐。
    motoko
    let x = someFunction(
      arg1, arg2, arg3, arg4, arg5);               // Do this.
    
    let x = someFunction(arg1, arg2, arg3,
      arg4, arg5);                                 // Or this.
    
    let x =
      someFunction(arg1, arg2, arg3, arg4, arg5);  // Or this.
    
    let x = someFunction(                          // Or this.
      longArg1,
      longArg2,
      longArg3,
      longArg4,
      longArg5,
    );
    
    // COUNTER EXAMPLE!
    let x = someFunction(arg1, arg2, arg3,
                         arg4, arg5);              // DO NOT DO THIS!
    理由:垂直对齐存在诸多问题,例如:
    1. 浪费大量水平空间。
    2. 导致缩进级别极度不一致,混淆代码结构。
    3. 修改某一行时会引发重新对齐的混乱,即使编辑器自动处理,也会使差异信息膨胀且难以理解。
    4. 在可变宽度字体下完全失效。
    经验法则:所有缩进都应是2的倍数。
  • 不要使用制表符(Tab)。
    理由:不同工具对制表符的解析差异极大,在网页、差异对比等很多场景中会丢失或显示错误。

Grouping

分组

  • Separate complex multi-line definitions with empty lines. One-liners can be put on consecutive lines.
    motoko
    func foo() {
      // This function does a lot of interesting stuff.
      // It's definition takes multiple lines.
    }
    
    func boo() {
      // This is another complicated function.
      // It's definition also takes multiple lines.
    }
    
    func add(x : Nat, y : Nat) { return x + y };
    func mul(x : Nat, y : Nat) { return x * y };
  • Separate logic groups of definitions with two empty lines. Add a one-line comment as a "section header" for each group.
    motoko
    // A very large class
    class MuffleMiff(n : Nat) {
    
      // Accessors
    
      public func miffMuff() : Text {
        ...
      }
    
      public func sniffMiff() : Nat {
        ...
      }
    
      // Mutators
    
      public func clearMurk() {
        ...
      }
    
      public func addMuff(name : Text) {
        ...
      }
    
      // Processing
    
      public func murkMuffle(param : List<Gnobble>) {
        ...
      }
    
      public func transformSneezler() {
        ...
      }
    
      // Internal State
    
      var miffCount = 0;
      var mabbleMap = Map<Nat, Text>();
    
    }
  • 复杂的多行定义之间用空行分隔。单行定义可以连续放置。
    motoko
    func foo() {
      // This function does a lot of interesting stuff.
      // It's definition takes multiple lines.
    }
    
    func boo() {
      // This is another complicated function.
      // It's definition also takes multiple lines.
    }
    
    func add(x : Nat, y : Nat) { return x + y };
    func mul(x : Nat, y : Nat) { return x * y };
  • 逻辑相关的定义组之间用两个空行分隔。为每个组添加一行注释作为“章节标题”。
    motoko
    // A very large class
    class MuffleMiff(n : Nat) {
    
      // Accessors
    
      public func miffMuff() : Text {
        ...
      }
    
      public func sniffMiff() : Nat {
        ...
      }
    
      // Mutators
    
      public func clearMurk() {
        ...
      }
    
      public func addMuff(name : Text) {
        ...
      }
    
      // Processing
    
      public func murkMuffle(param : List<Gnobble>) {
        ...
      }
    
      public func transformSneezler() {
        ...
      }
    
      // Internal State
    
      var miffCount = 0;
      var mabbleMap = Map<Nat, Text>();
    
    }

Comments

注释

  • Use line comments (
    //…​
    ). Use block comments (
    /* …​ */
    ) only when commenting in the middle of a line or for commenting out pieces of code during development.
    motoko
    // The following function runs the current
    // pallaboom on a given snibble. It returns
    // suitable plexus if it can.
    func paBoom(s : Snibble) : Handle<Plexus> {
      let puglet = initPugs(s.crick, 0 /* size */, #local);
    /* Don't do the odd stuff yet...
      ...
      ...
    */
      return polyfillNexus(puglet);  // for now
    }
    Rationale: Line comments make it easier to insert, remove or swap individual lines.
  • Put short comments explaining a single line at the end of the line, separated by at least 2 spaces.
    motoko
    paBoom(getSnibble()));  // create new snibble
  • Put multi-line comments before a line of code, with the same indentation as the code it is describing.
    motoko
    func f() {
      // Try to invoke the current pallaboom with
      // the previous snibble. If that succeeds,
      // we have the new plexus; if not, complain.
      let plexusHandle = paBoom(getSnibble()));
    }
  • Capitalize comments that are on separate lines. Use a proper full stop for sentences.
  • 使用行注释(
    //…​
    )。仅在注释行内内容或开发期间注释代码块时使用块注释(
    /* …​ */
    )。
    motoko
    // The following function runs the current
    // pallaboom on a given snibble. It returns
    // suitable plexus if it can.
    func paBoom(s : Snibble) : Handle<Plexus> {
      let puglet = initPugs(s.crick, 0 /* size */, #local);
    /* Don't do the odd stuff yet...
      ...
      ...
    */
      return polyfillNexus(puglet);  // for now
    }
    理由:行注释更便于插入、删除或交换单行内容。
  • 解释单行代码的简短注释放在行尾,与代码之间至少隔2个空格。
    motoko
    paBoom(getSnibble()));  // create new snibble
  • 多行注释放在代码行之前,缩进级别与所描述的代码一致。
    motoko
    func f() {
      // Try to invoke the current pallaboom with
      // the previous snibble. If that succeeds,
      // we have the new plexus; if not, complain.
      let plexusHandle = paBoom(getSnibble()));
    }
  • 单独成行的注释首字母大写,句子结尾使用句号。

Punctuation

标点

Semicolons

分号

  • Motoko uniformly requires a semicolon to separate expressions or local declarations in a block, regardless of whether the preceding declaration ends in a closing '}'.
    Rationale: This is unlike other C-style languages, which tend to have rather ad-hoc rules.
  • Put a semicolon after the last expression in a block, unless the whole block is written on a single line.
    Similarly for types.
    motoko
    // No ; needed before closing } on same line
    
    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 };
    
    // End last case with ;
    
    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) };
      };
    }
    Rationale: Consistently ending lines with semicolon simplifies adding, removing, or swapping lines.
  • Motoko统一要求在块中的表达式或局部声明之间使用分号分隔,无论前一个声明是否以闭合
    }
    结尾。
    理由:这与其他C风格语言不同,那些语言的规则往往较为随意。
  • 块中的最后一个表达式后需加分号,除非整个块写在同一行。
    类型定义同理。
    motoko
    // No ; needed before closing } on same line
    
    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 };
    
    // End last case with ;
    
    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) };
      };
    }
    理由:始终在行尾加分号可简化添加、删除或交换行的操作。

Braces

大括号

  • Put braces around function bodies,
    if
    or
    case
    branches, and loop bodies, unless they appear nested as an expression and only contain a single expression.
    motoko
    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;
  • Use "C-style" layout for braced sub-expressions stretching multiple lines.
    motoko
    func f() {
      return;
    };
    
    if (cond) {
      foo();
    } else {
      bar();
    };
    
    switch (opt) {
      case (?x) {
        f(x);
      };
      case (null) {};
    };
  • 函数体、
    if
    case
    分支、循环体需用大括号包裹,除非它们作为表达式嵌套且仅包含单个表达式。
    motoko
    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风格”布局。
    motoko
    func f() {
      return;
    };
    
    if (cond) {
      foo();
    } else {
      bar();
    };
    
    switch (opt) {
      case (?x) {
        f(x);
      };
      case (null) {};
    };

Parentheses

括号

  • Motoko supports "parenless" style, meaning that parentheses are optional in most places, such as function parameter lists, or statement operands, when they enclose an expression that either is bracketed already. For example, a tuple, object, or array, or a simple constant or identifier.
    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() };
    };
  • Avoid overuse of parenless style.
    In particular, do not omit parentheses and braces on statements at the same time.
    motoko
    // COUNTER EXAMPLES!
    let choice = if flag x + y else z;  // DO NOT DO THIS!
    
    switch val {
      case 0 f();    // DO NOT DO THIS!
      case n n + 1;  // OR THIS!
    };
    Rationale: Omitting both at the same time makes the code harder to read, since there is less visual clue how it groups.
  • Similarly, do not omit parentheses around function parameters if the function also has type parameters.
    motoko
    // COUNTER EXAMPLE!
    foo<Nat> 0;   // DO NOT DO THIS!
  • Omit parentheses around argument types of a function type with a single argument and no type parameters.
    But do not omit them around when functions or classes also have type parameters.
    motoko
    type Inv = Nat -> Nat;
    type Id = <T>(T) -> T;
    type Get = <X>(C<X>) -> X;
    
    // COUNTER EXAMPLE!
    type Get = <X>C<X> -> X;   // DO NOT DO THIS!
  • Motoko支持“无括号”风格,即大多数情况下括号是可选的,例如函数参数列表或语句操作数,当它们包裹的表达式已经是带括号的(如元组、对象或数组)或简单常量/标识符时。
    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() };
    };
  • 避免过度使用无括号风格。
    尤其不要同时省略语句的括号和大括号。
    motoko
    // COUNTER EXAMPLES!
    let choice = if flag x + y else z;  // DO NOT DO THIS!
    
    switch val {
      case 0 f();    // DO NOT DO THIS!
      case n n + 1;  // OR THIS!
    };
    理由:同时省略两者会使代码更难阅读,因为视觉上缺乏分组线索。
  • 同理,如果函数带有类型参数,不要省略函数参数的括号。
    motoko
    // COUNTER EXAMPLE!
    foo<Nat> 0;   // DO NOT DO THIS!
  • 对于只有单个参数且无类型参数的函数类型,省略参数类型的括号。
    但当函数或类带有类型参数时,不要省略括号。
    motoko
    type Inv = Nat -> Nat;
    type Id = <T>(T) -> T;
    type Get = <X>(C<X>) -> X;
    
    // COUNTER EXAMPLE!
    type Get = <X>C<X> -> X;   // DO NOT DO THIS!

Miscellaneous

其他

  • Use
    _
    to group digits in numbers.
    Group by 3 digits in decimal numbers and by 4 in hexadecimal notation.
    motoko
    let billion = 1_000_000_000;
    let pi = 3.141_592_653_589_793_12;
    let mask : Nat32 = 0xff00_ff0f;
  • 使用
    _
    对数字中的数字进行分组。
    十进制数字按3位分组,十六进制按4位分组。
    motoko
    let billion = 1_000_000_000;
    let pi = 3.141_592_653_589_793_12;
    let mask : Nat32 = 0xff00_ff0f;

Naming

命名

Style

风格

  • Use
    UpperCamelCase
    for type names (including classes or type parameters), module names, and actor names.
  • Use
    lowerCamelCase
    for all other names, including constants and variant fields.
    motoko
    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 { ... };
    };
    Rationale: The general convention is upper case for "static" entities like types and lower case for "dynamic" values. Modules and actors are fairly static and can export types. Objects usually don’t export types and tend to be used mostly as dynamic values.
  • Spell acronyms as regular words.
    motoko
    type HttpHeader = ...;
    func getUrl() { ... };
    let urlDigest = ...;
  • Do not use identifier names that start with an underscore
    _
    , except to document that a variable in a pattern is intentionally unused.
    motoko
    let (width, _color, name) = rumpler();
    ...  // _color is not used here
    
    func foo(x : Nat, _futureFlag : Bool) { ... };
    Rationale: A type checker can warn about unused identifiers, which can be suppressed by explicitly prepending
    _
    to its name to document intention.
    This aligns with the use of the keyword
    _
    for pattern wildcards.
  • 类型名称(包括类或类型参数)、模块名称和actor名称使用
    UpperCamelCase
    (大驼峰命名法)。
  • 所有其他名称使用
    lowerCamelCase
    (小驼峰命名法),包括常量和变体字段。
    motoko
    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相对静态,可导出类型。对象通常不导出类型,主要用作动态值。
  • 缩写词按常规单词拼写。
    motoko
    type HttpHeader = ...;
    func getUrl() { ... };
    let urlDigest = ...;
  • 不要使用以下划线
    _
    开头的标识符名称,除非用于标记模式中故意未使用的变量。
    motoko
    let (width, _color, name) = rumpler();
    ...  // _color is not used here
    
    func foo(x : Nat, _futureFlag : Bool) { ... };
    理由:类型检查器会对未使用的标识符发出警告,可通过在名称前显式添加
    _
    来标记意图,从而抑制警告。
    这与模式通配符中关键字
    _
    的用法一致。

Conventions

约定

  • The name of functions returning a value should describe that value.
    Avoid redundant
    get
    prefixes.
    motoko
    dict.size();
    list.first();
    sum(array);
  • The name of functions performing side effects or complex operations should describe that operation.
    motoko
    dict.clear();
    dict.set(key, value);
    let result = traverse(graph);
  • The name of predicate functions returning
    Bool
    should use an
    is
    or
    has
    prefix or a similar description of the tested property.
    motoko
    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 { ... };
    };
  • Functions converting to or from a type
    X
    are named
    toX
    and
    fromX
    , respectively, if the source, resp. target, is either the object the function is a method of, or the primary type of the module this function appears in.
  • In classes or objects, use a name ending with
    _
    to distinguish private variables from getters.
    motoko
    class Cart(length_ : Nat) {
      var width_ = 0;
    
      public func length() : Nat { return length_ };
      public func width() : Nat { return width_ };
    }
    Rationale: In Motoko, functions are first-class values, so functions and other value identifiers share the same name space.
    Identifiers with a leading
    _
    should not be used for private state, since that indicates an unused name (see style).
  • Use longer, more descriptive names for global or public identifier or ones with large scope, and short names for local ones with small scope.
    It is fine to use single character identifiers when there is nothing interesting to say, especially when using the same naming scheme consistently.
    motoko
    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;
    };
    Rationale: Contrary to popular belief, overly chatty local names can decrease readability instead of increasing it, by increasing the noise level.
  • In suitable cases, use plural form for describing a collection of items, such as a list or array.
    This also works for short names.
    motoko
    func foreach<X>(xs : [X], f : X -> ()) {
      for (x in xs.values()) { f(x) }
    }
  • 返回值的函数名称应描述该返回值。
    避免冗余的
    get
    前缀。
    motoko
    dict.size();
    list.first();
    sum(array);
  • 执行副作用或复杂操作的函数名称应描述该操作。
    motoko
    dict.clear();
    dict.set(key, value);
    let result = traverse(graph);
  • 返回
    Bool
    类型的谓词函数应使用
    is
    has
    前缀,或类似描述被测属性的词汇。
    motoko
    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
    转换的函数分别命名为
    toX
    fromX
    ,如果源类型或目标类型是函数所属对象的类型,或函数所在模块的主类型。
  • 在类或对象中,使用以
    _
    结尾的名称区分私有变量与 getter 方法。
    motoko
    class Cart(length_ : Nat) {
      var width_ = 0;
    
      public func length() : Nat { return length_ };
      public func width() : Nat { return width_ };
    }
    理由:在Motoko中,函数是一等公民,因此函数和其他值标识符共享同一命名空间。
    以下划线开头的标识符不应用于私有状态,因为这表示未使用的名称(参见风格)。
  • 全局或公共标识符、作用域大的标识符使用更长、更具描述性的名称,作用域小的局部标识符使用短名称。
    当没有特别需要说明的内容时,使用单字符标识符是可以的,尤其是在一致使用相同命名方案的情况下。
    motoko
    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;
    };
    理由:与普遍看法相反,过于冗长的局部名称会增加噪音,反而降低可读性。
  • 在合适的情况下,使用复数形式描述项目集合,如列表或数组。
    短名称也适用此规则。
    motoko
    func foreach<X>(xs : [X], f : X -> ()) {
      for (x in xs.values()) { f(x) }
    }

Types

类型

Type annotations

类型注解

  • Put type annotations on definitions that involve fixed-width numeric types, to disambiguate the type of overloaded arithmetic operators and constants.
    motoko
    let mask : Nat32 = 0xfc03_ff00;
    let pivot : Nat32 = (size + 1)/2;
    let vec : [Int16] = [1, 3, -4, 0];
    :::note
    Use floating point constants to enforce type
    Float
    without an extra annotation. Similarly, use an explicit
    +
    sign to produce a positive value of type
    Int
    instead of
    Nat
    , if desired.
    :::
    motoko
    let zero = 1.0;    // type Float
    let offset = +1;   // type Int
  • Similarly, put inline type annotations on arithmetic expressions with types other than
    Nat
    or
    Int
    .
    motoko
    if (x & mask == (1 : Nat32)) { ... };
    :::note
    The need to annotate constants in cases like this is a short-coming of Motoko’s type system that we hope to address soon.
    :::
    An annotation is not needed on function arguments, since their type is usually inferred from the function. The only exception is when that argument has generic type and the type arguments have been omitted.
    motoko
    func foo(len : Nat32, vec : [Nat16]) { ... };
    func bar<X>(x : X) { ... };
    
    foo(3, [0, 1, 2]);
    bar<Nat16>(0);
    bar(0 : Nat16);
  • Put type annotations on mutable variables, unless their type is obvious.
    motoko
    var name = "Motoko";
    var balance = 0;
    
    func f(i : Int) {
      var j = i;
    };
    
    var balance : Int = 0;
    var obj : Class = foo();
    Rationale: Due to subtyping, inferring the type from the initialization would not necessarily deduce the intended type. For example,
    balance
    would have type
    Nat
    without the annotation, ruling out assignments of integers.
  • Put type annotations on all public fields in a class.
    motoko
    class C(init_ : Nat) {
      public let init : Nat = init_;
      public var count : Nat = 0;
    }
  • Omit return type annotations of functions when the type is
    ()
    .
    motoko
    func twiceF() { f(); f() };  // no need to write ": ()"
  • Omit type annotations on functions when they are passed as arguments.
    motoko
    Array.map<Nat, Nat>(func n {n + 1}, a);
  • Put type annotations on definitions that involve numeric types other than
    Nat
    or
    Int
    , to resolve the overloading between arithmetic operators and constants.
    motoko
    let mask : Nat32 = 0xfc03_ff00;
    let offset : Nat32 = size + 1;
  • 涉及固定宽度数值类型的定义需添加类型注解,以消除重载算术运算符和常量的类型歧义。
    motoko
    let mask : Nat32 = 0xfc03_ff00;
    let pivot : Nat32 = (size + 1)/2;
    let vec : [Int16] = [1, 3, -4, 0];
    :::note
    使用浮点常量来强制
    Float
    类型,无需额外注解。同理,如果需要,使用显式的
    +
    号生成
    Int
    类型的正值,而非
    Nat
    类型。
    :::
    motoko
    let zero = 1.0;    // type Float
    let offset = +1;   // type Int
  • 同理,对
    Nat
    Int
    以外类型的算术表达式添加内联类型注解。
    motoko
    if (x & mask == (1 : Nat32)) { ... };
    :::note
    在这种情况下需要注解常量是Motoko类型系统的一个不足,我们希望尽快解决。
    :::
    函数参数不需要注解,因为它们的类型通常可从函数推断得出。唯一的例外是参数为泛型类型且已省略类型参数时。
    motoko
    func foo(len : Nat32, vec : [Nat16]) { ... };
    func bar<X>(x : X) { ... };
    
    foo(3, [0, 1, 2]);
    bar<Nat16>(0);
    bar(0 : Nat16);
  • 可变变量需添加类型注解,除非其类型显而易见。
    motoko
    var name = "Motoko";
    var balance = 0;
    
    func f(i : Int) {
      var j = i;
    };
    
    var balance : Int = 0;
    var obj : Class = foo();
    理由:由于子类型的存在,从初始化值推断类型不一定能得出预期类型。例如,不加注解的
    balance
    类型为
    Nat
    ,这会排除整数赋值。
  • 类的所有公共字段需添加类型注解。
    motoko
    class C(init_ : Nat) {
      public let init : Nat = init_;
      public var count : Nat = 0;
    }
  • 当函数返回类型为
    ()
    时,省略返回类型注解。
    motoko
    func twiceF() { f(); f() };  // no need to write ": ()"
  • 函数作为参数传递时,省略类型注解。
    motoko
    Array.map<Nat, Nat>(func n {n + 1}, a);
  • 涉及
    Nat
    Int
    以外数值类型的定义需添加类型注解,以解决算术运算符和常量的重载问题。
    motoko
    let mask : Nat32 = 0xfc03_ff00;
    let offset : Nat32 = size + 1;

Picking types

类型选择

  • Use
    Nat
    for any integral value that cannot be negative.
  • Use fixed-width
    NatN
    or
    IntN
    only when storing many values and space usage matters, when bit-fiddling requires the low-level interpretation of a number as a vector of bits or when matching types imposed by external requirements, such as other canisters.
  • Avoid proliferation of option types, and therefore
    null
    .
    Limit their use to as small a scope as possible. Rule out the
    null
    case and use non-option types wherever possible.
  • Consider using records instead of tuples when there are more than 2 or 3 components. Records are just simple objects with named fields.
    Note that record types need not be declared but can be used in place.
    motoko
      func nodeInfo(node : Node) : {parent : Node; left : Node; right : Node} { ... }
  • Consider using variants instead of
    Bool
    to represent binary choices.
    Note that variant types need not be declared but can be used in place.
    motoko
    func capitalization(word : Text) : {#upper; #lower} { ... }
  • Where possible, use return type
    ()
    for functions whose primary purpose is to mutate state or cause other side effects.
    motoko
    class Set<X>() {
      public func add(x : X) { ... };
      public func remove(x : X) { ... };
      ...
    };
  • Consider using a record (an object with just data) as argument for long parameter lists.
    motoko
    func process({seed : Float; delta : Float; data : [Record]; config : Config}) : Thing {
      ...
    };
    
    process{config = Config(); data = read(); delta = 0.01; seed = 1.0};
    Rationale: This expresses named parameters. This way, arguments can be freely reordered at the call site and callers are prevented from accidentally passing them in the wrong order.
  • Higher-order functions, such as functions that take a callback argument, should put the function parameter last.
    Rationale: Makes call sites more readable, and in the absence of currying, there is no point in putting the function first, like you often would in functional languages.
  • Do not use sentinel values, such as
    -1
    , to represent invalid values.
    Use the option type instead.
    motoko
    func lookup(x : key) : ?Nat { ... }
  • Data is immutable in Motoko unless explicitly stated otherwise.
    Use mutability types and definitions (
    var
    ) with care and only where needed.
    Rationale: Mutable data cannot be communicated or share across actors. It is more error-prone and much more difficult to formally reason about, especially when concurrency is involved.
  • 任何非负整数值使用
    Nat
    类型。
  • 仅当存储大量值且空间使用很重要、位操作需要将数字作为位向量进行低级解释,或匹配外部要求(如其他canister)的类型时,才使用固定宽度的
    NatN
    IntN
    类型。
  • 避免过度使用可选类型(option type),即
    null
    将其使用范围限制在尽可能小的范围内。尽可能排除
    null
    情况,使用非可选类型。
  • 当组件数量超过2或3个时,考虑使用记录而非元组。记录就是带有命名字段的简单对象。
    注意:记录类型无需声明,可直接使用。
    motoko
      func nodeInfo(node : Node) : {parent : Node; left : Node; right : Node} { ... }
  • 考虑使用变体类型而非
    Bool
    来表示二元选择。
    注意:变体类型无需声明,可直接使用。
    motoko
    func capitalization(word : Text) : {#upper; #lower} { ... }
  • 尽可能将主要目的是修改状态或产生其他副作用的函数返回类型设为
    ()
    motoko
    class Set<X>() {
      public func add(x : X) { ... };
      public func remove(x : X) { ... };
      ...
    };
  • 考虑使用记录(仅包含数据的对象)作为长参数列表的参数。
    motoko
    func process({seed : Float; delta : Float; data : [Record]; config : Config}) : Thing {
      ...
    };
    
    process{config = Config(); data = read(); delta = 0.01; seed = 1.0};
    理由:这表示命名参数。这样调用时参数可自由重新排序,避免调用者意外传递错误顺序的参数。
  • 高阶函数(如接受回调参数的函数)应将函数参数放在最后。
    理由:使调用点更易读,且在没有柯里化的情况下,像函数式语言那样将函数放在前面没有意义。
  • 不要使用哨兵值(如
    -1
    )表示无效值。
    改用可选类型。
    motoko
    func lookup(x : key) : ?Nat { ... }
  • Motoko中的数据默认是不可变的,除非显式声明为可变。
    谨慎使用可变类型和定义(
    var
    ),仅在必要时使用。
    理由:可变数据无法在actor之间通信或共享。它更容易出错,尤其是在涉及并发时,更难进行形式化推理。

Features

特性

Statements

语句

  • Use
    for
    loops instead of
    while
    loops for iterating over a numeric range or a container.
    motoko
    for (i in Iter.range(1, 10)) { ... };
    for (x in array.values()) { ... };
    Rationale: For loops are less error-prone and easier to read.
  • Use
    if
    or
    switch
    as expressions where appropriate.
    motoko
    func abs(i : Int) : Int { if (i < 0) -i else i };
    
    let delta = switch mode { case (#up) +1; case (#dn) -1 };
  • Motoko requires that all expressions in a block have type
    ()
    , in order to prevent accidentally dropped results.
    Use
    ignore
    to explicitly drop results. Do not use
    ignore
    when it’s not needed.
    motoko
    ignore async f();  // fire of a computation
  • Motoko allows to omit the
    return
    at the end of a function, because a block evaluates to its last expression.
    Use this when a function is short and in "functional" style, that is, the function does not contain complex control flow or side effects.
    Use explicit
    return
    at the end when the function contains other
    return
    statements or imperative control flow.
    motoko
    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;
    };
  • 遍历数值范围或容器时,使用
    for
    循环而非
    while
    循环。
    motoko
    for (i in Iter.range(1, 10)) { ... };
    for (x in array.values()) { ... };
    理由:
    for
    循环更不易出错,且更易阅读。
  • 在合适的情况下将
    if
    switch
    用作表达式。
    motoko
    func abs(i : Int) : Int { if (i < 0) -i else i };
    
    let delta = switch mode { case (#up) +1; case (#dn) -1 };
  • Motoko要求块中的所有表达式类型为
    ()
    ,以防止意外丢弃结果。
    使用
    ignore
    显式丢弃结果。不需要时不要使用
    ignore
    motoko
    ignore async f();  // fire of a computation
  • Motoko允许省略函数末尾的
    return
    ,因为块的计算结果为其最后一个表达式。
    当函数较短且为“函数式”风格(即函数不包含复杂控制流或副作用)时,使用此特性。
    当函数包含其他
    return
    语句或命令式控制流时,在末尾使用显式
    return
    motoko
    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;
    };

Objects and records

对象与记录

  • Use the short-hand object syntax
    {x1 = e1; …​ ; xN = eN}
    when using objects as simple records, i.e., data structures with no private state and no methods.
  • Use
    object
    when creating singleton objects.
  • Limit the use of objects to records where possible.
    Rationale: Only records can be sent as message parameters or results and can be stored in stable variables. Objects with methods are also more expensive to create and represent in memory.
  • Use full objects only as a means for encapsulating state or behavior.
  • 当将对象用作简单记录(即无私有状态和方法的数据结构)时,使用简写对象语法
    {x1 = e1; …​ ; xN = eN}
  • 创建单例对象时使用
    object
  • 尽可能将对象的使用限制为记录。
    理由:只有记录可作为消息参数或结果发送,并可存储在稳定变量中。带方法的对象创建和内存表示成本更高。
  • 仅将完整对象用于封装状态或行为。

Classes

  • Use
    class
    to create multiple objects of the same shape.
  • Name classes after their conceptual functionality, not their implementation, except when having to distinguish multiple different implementations of the same concept. For example,
    OrderedMap
    vs
    HashMap
    ).
  • Classes are both type definitions and factory functions for objects.
    Do not use classes unless both these roles are intended; use plain type aliases or functions returning an object in other cases.
  • Do not overuse classes.
    Use a module defining a plain type and functions on it where appropriate. Use classes only as a means for encapsulating state or behavior.
    Rationale: Objects with methods have disadvantages over simple record types with separate functions (see above).
  • If values of a class are meant to be sendable (shared), the class needs to provide a pair of
    share
    /
    unshare
    methods that convert to/from a sharable representation, for example, as a record.
    :::note
    For immutable classes it may seem more natural to make
    unshare
    a kind of static function. However, even for immutable ones it may depend on constructor arguments (such as an ordering function), so that the a pattern like
    Map(compareInt).unshare(x)
    seems appropriate.
    :::
  • For the time being, avoid overloading classes with too many methods, since that is currently expensive.
    Restrict to a sufficiently small set of canonical methods and make less essential ones that can be implemented on top of those into functions in the enclosing module.
  • Use modules for "static" classes or methods.
  • 使用
    class
    创建多个相同结构的对象。
  • 类的名称应描述其概念功能,而非实现,除非必须区分同一概念的多个不同实现(例如
    OrderedMap
    HashMap
    )。
  • 类既是类型定义,也是对象的工厂函数。
    除非同时需要这两种角色,否则不要使用类;其他情况使用普通类型别名或返回对象的函数。
  • 不要过度使用类。
    合适的情况下,使用定义普通类型及其相关函数的模块。仅将类用于封装状态或行为。
    理由:带方法的对象相较于带独立函数的简单记录类型存在诸多缺点(参见上文)。
  • 如果类的实例需要可发送(共享),则类需提供一对
    share
    /
    unshare
    方法,用于在可共享表示(例如记录)之间转换。
    :::note
    对于不可变类,将
    unshare
    设为静态函数似乎更自然。然而,即使是不可变类,它也可能依赖构造函数参数(如排序函数),因此像
    Map(compareInt).unshare(x)
    这样的模式更为合适。
    :::
  • 目前,避免给类添加过多方法,因为这会增加开销。
    限制为足够小的一组标准方法,将可基于这些方法实现的非必要方法放在外围模块中作为函数。
  • 使用模块实现“静态”类或方法。

Modules

模块

  • Use
    module
    to group definitions, including types, and create a name spae for them.
  • Where applicable, name modules after the main type or class they implement or provide functions for.
  • Limit each module to a single main concept/type/class or closely entangled family of concepts/types/classes.
  • 使用
    module
    对定义(包括类型)进行分组,并为它们创建命名空间。
  • 适用时,模块名称以其实现或提供函数的主要类型或类命名。
  • 每个模块限制为单个主要概念/类型/类,或紧密相关的概念/类型/类家族。