控制流有两个关键类别:

  • 声明性(declarative):当某些值的结构引导控件和下一个要计算的表达式的选择时,如 ifswitch 表达式;
  • 命令式(Imperative):根据程序员的命令突然改变,禁止常规控制流; 例如 breakcontinue ,以及 returnthrow

命令式(Imperative)控制流通常与状态更改和其他side-effects(如错误处理和输入/输出)密切相关。

Early return from func

通常,函数的结果是函数体的值。有时候,在对函数体(body)进行评估的过程中,评估结果在评估结束之前就可以得到。在这种情况下,可以使用 return <exp> 结构放弃计算的其余部分,并立即退出函数得到一个结果。类似地,在允许的情况下,throw可用于放弃带有错误的计算。

当函数具有单位结果类型时,可以使用简写return代替等效的 return ()

Loops and labels:

Motoko提供了几种循环的结构,包括:

  • for表达式用于迭代结构化数据成员。
  • loop表达式用于编程重复(可选带终止条件)的循环。
  • while loops for programmatic repetition with entry condition.While循环用于带有入口条件的编程重复。

其中任何一个都可以用一个label <name>作为前缀,以便给循环一个符号名。命名循环用于强制更改控制流以从命名循环的入口或出口继续。

  • continue <name>重新进入循环,或
  • break <name>完全退出循环。

在下面的示例中,for 表达式循环遍历一些文本的字符,并在遇到感叹号时放弃迭代。

import Debug "mo:base/Debug";
label letters for (c in "ran!!dom".chars()) {
  Debug.print(debug_show(c));
  if (c == '!') { break letters };
  // ...
}

Labeled expressions:

还有另外两个不太主流但在某些情况下很有用的label

  • 可以输入label
  • 任何表达式(不仅限于循环)都可以通过给它添加label前缀来命名;break 允许通过为其结果提供一个直接值来短路(short-circuit)表达式的计算。(这类似于使用 return 提前退出函数,但是没有声明和调用函数的开销。)

带类型注释的标签的语法是label <name> : <type> <expr>。表示任何表达式都可以使用break <name> <alt-expr>结构退出,该结构返回<alt-expr> 的值作为<expr>的值,对 <expr>进行短路(short-circuit)计算。

明智地使用这些结构允许程序员专注于主程序逻辑,并通过 break 处理异常情况。

import Text "mo:base/Text";
import Iter "mo:base/Iter";

type Host = Text;
let formInput = "us@dfn";

let address = label exit : ?(Text, Host) {
  let splitted = Text.split(formInput, #char '@');
  let array = Iter.toArray<Text>(splitted);
  if (array.size() != 2) { break exit(null) };
  let account = array[0];
  let host = array[1];
  // if (not (parseHost(host))) { break exit(null) };
  ?(account, host)
}

自然地,被label标记的普通表达式不允许continue.在类型方面,<expr>和 <alt-expr>​的类型必须符合label的<type>定义。如果一个标签只被赋予了一个<name>, 那么它的 <type>默认为元组(unit)(()). 类似地,没有 <alt-expr> 表达式的break 是返回值unit(())的break表达式的简写。

Option blocks and null breaks:

像许多其他高级语言一样,Motoko 允许您选择null值,跟踪在使用表单的选项类型?Tnull值的可能发生情况。这既是为了鼓励您尽可能避免使用null值,也是为了在必要时考虑null值的可能性。

如果测试null值的唯一方法是使用详细的switch表达式,那么后者可能会很麻烦,但 Motoko 使用一些专门的语法简化了对选项类型的处理: option blocksnull break

选项块do ? <block>产生一个?T类型的值,同时块<block>的类型为 T,并且重要的是,引出了来自<block>的一个break的可能性。在一个 do ? <block>里,the null break<exp> !(PS:下方重点记号处有讲解)测试了表达式<exp>的结果?U(U为一个不相关的类型)是否为null。如果<exp>的结果是null,则控制立刻携带着null值离开do ? <block>。否则,<exp>的结果一定是一个选项类型值?v,并且<exp> ! 会继续对?v的内容v(类型为U)进行评估。

——原文为"The option block, do ? <block>, produces a value of type ?T, when block <block> has type T and, importantly, introduces the possibility of a break from <block>. Within a do ? <block>, the null break <exp> !, tests whether the result of the expression, '<exp>', of unrelated option type, ?U, is null. If the result <exp>is null, control immediately exits the do ? <block> with value null. Otherwise, the result of <exp> must be an option value ?v, and evaluation of <exp> ! proceeds with its contents, v (of type U)."

作为一个实际的例子,我们给出了一个简单的函数 eval 的数值 Exp 表达式的定义,这个表达式是由自然数、除法和零测试构成的,编码为一个变量类型:

type Exp = {
  #Lit : Nat;
  #Div : (Exp, Exp);
  #IfZero : (Exp, Exp, Exp);
};

func eval(e : Exp) : ? Nat {
  do ? {
    switch e {
      case (#Lit n) { n };
      case (#Div (e1, e2)) {
        let v1 = eval e1 !;
        let v2 = eval e2 !;
        if (v2 == 0)
          null !
        else v1 / v2
      };
      case (#IfZero (e1, e2, e3)) {
        if (eval e1 ! == 0)
          eval e2 !
        else
          eval e3 !
      };
    };
  };
}

为了防止在不捕获的情况下被除0eval 函数返回一个选项结果,使用 null 表示失败。

每个递归调用都使用!(单目运算符,前加一个<exp>组成null break,个人的理解是当操作数为null时,“返回”break,退出当前控制流/块)。当结果为 null 时,立即退出外层的do ? block,也因此退出函数本身,以null为返回值。

Repetition with loop

无限重复命令表达式序列的最简单方法是使用loop循环结构。

loop { ⟨expr1⟩; ⟨expr2⟩; ... }

只能使用 returnbreak 结构退出循环。

可以附加一个重返条件,允许有条件地重复使用 loop <body> while <cond>循环。

特点:这种循环的主体(body)总是至少执行一次

while loops with precondition:

有时需要一个入口条件来保护循环的第一次执行。对于这种循环,可以使用while <cond> <body>结构。

while (earned < need) { earned += earn() };

特点:与loop循环不同,while循环的主体可能永远不会执行

for loops for iteration:

可以使用 for 循环对某个同类集合的元素进行迭代。这些值从迭代器中获取,并依次绑定到循环模式(pattern)。

let carsInStock = [
  ("Buick", 2020, 23.000),
  ("Toyota", 2019, 17.500),
  ("Audi", 2020, 34.900)
];
var inventory : { var value : Float } = { var value = 0.0 };
for ((model, year, price) in carsInStock.vals()) {
  inventory.value += price;
};
inventory

Using range with a for loop:

Range 函数生成一个迭代器(Iter<Nat>类型) ,其中包含给定的上下界。

下面的循环示例在十一次迭代中输出数字0到10:

import Iter "mo:base/Iter";
import Debug "mo:base/Debug";
var i = 0;
for (j in Iter.range(0, 10)) {
  Debug.print(debug_show(j));
  assert(j == i);
  i += 1;
};
assert(i == 11);

更一般地说,range函数是一个在自然数序列上构造迭代器的class,调用后返回一个迭代器,每个这样的迭代器都为Iter<Nat>类型。

作为一个构造函数,range 有一个函数类型:

(lower:Nat, upper:Nat) -> Iter<Nat>

其中 Iter<Nat>是一个迭代器对象类型,next方法生成可选的元素,每个元素类型为?Nat

type Iter<A> = {next : () -> ?A};

对于每次调用,next返回一个可选元素(类型为?Nat)。

null 表示迭代序列已终止。

到达null之前,每个形式为 ?n 的非null值中的 n 包含迭代序列中的下一个连续元素。

Using revrange

Like range, the function revrange is a class that constructs iterators (each of type Iter<Nat>). As a constructor function, it has a function type:

range类似,revrange函数是一个构造迭代器(每个类型都为 Iter<Nat>)的类。作为一个构造函数,它有一个函数类型:

(upper:Nat, lower:Nat) -> Iter<Nat>

不像rangerevrange函数在其迭代序列中递减,从初始上界到最终下界。

Using iterators of specific data structures:

许多内置的数据结构都有预定义的迭代器:

TypeNameIteratorElementsElement type
[T]array of T​svalsthe array’s membersT
[T]array of T​skeysthe array’s valid indicesNat
[var T]mutable array of T​svalsthe array’s membersT
[var T]mutable array of T​skeysthe array’s valid indicesNat
Texttextcharsthe text’s charactersChar
Blobblobvalsthe blob’s bytesNat8

用户定义的数据结构可以定义自己的迭代器。只要它们符合元素类型A对应的的Iter<A>类型,它们的行为就像内置的元素,可以与普通 for循环一起使用。

Unknown-54
- Motoko -

A Student on the way to full stack of Web3.