控制流有两个关键类别:
- 声明性(declarative):当某些值的结构引导控件和下一个要计算的表达式的选择时,如 if 和 switch 表达式;
- 命令式(Imperative):根据程序员的命令突然改变,禁止常规控制流; 例如 break 和 continue ,以及 return 和 throw 。
命令式(Imperative)控制流通常与状态更改和其他side-effects(如错误处理和输入/输出)密切相关。
Early return from func:
通常,函数的结果是函数体的值。有时候,在对函数体(body)进行评估的过程中,评估结果在评估结束之前就可以得到。在这种情况下,可以使用 return <exp> 结构放弃计算的其余部分,并立即退出函数得到一个结果。类似地,在允许的情况下,throw可用于放弃带有错误的计算。
当函数具有单位结果类型时,可以使用简写return代替等效的 return ()。
Loops and labels:
Motoko提供了几种循环的结构,包括:
for表达式用于迭代结构化数据成员。loop表达式用于编程重复(可选带终止条件)的循环。whileloops 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值,跟踪在使用表单的选项类型?T时null值的可能发生情况。这既是为了鼓励您尽可能避免使用null值,也是为了在必要时考虑null值的可能性。
如果测试null值的唯一方法是使用详细的switch表达式,那么后者可能会很麻烦,但 Motoko 使用一些专门的语法简化了对选项类型的处理: option blocks和 null 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 !
};
};
};
}
为了防止在不捕获的情况下被除0,eval 函数返回一个选项结果,使用 null 表示失败。
每个递归调用都使用!(单目运算符,前加一个<exp>组成null break,个人的理解是当操作数为null时,“返回”break,退出当前控制流/块)。当结果为 null 时,立即退出外层的do ? block,也因此退出函数本身,以null为返回值。
Repetition with loop:
无限重复命令表达式序列的最简单方法是使用loop循环结构。
loop { ⟨expr1⟩; ⟨expr2⟩; ... }
只能使用 return 或 break 结构退出循环。
可以附加一个重返条件,允许有条件地重复使用 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>
不像range,revrange函数在其迭代序列中递减,从初始上界到最终下界。
Using iterators of specific data structures:
许多内置的数据结构都有预定义的迭代器:
| Type | Name | Iterator | Elements | Element type |
|---|---|---|---|---|
[T] | array of Ts | vals | the array’s members | T |
[T] | array of Ts | keys | the array’s valid indices | Nat |
[var T] | mutable array of Ts | vals | the array’s members | T |
[var T] | mutable array of Ts | keys | the array’s valid indices | Nat |
Text | text | chars | the text’s characters | Char |
Blob | blob | vals | the blob’s bytes | Nat8 |
用户定义的数据结构可以定义自己的迭代器。只要它们符合元素类型A对应的的Iter<A>类型,它们的行为就像内置的元素,可以与普通 for循环一起使用。










Comments 1 条评论
Usually I do not learn post on blogs, but I wish to say that this write-up very pressured me to try and do so! Your writing style has been surprised me. Thanks, quite great post.