控制流有两个关键类别:
- 声明性(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
表达式用于编程重复(可选带终止条件)的循环。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
值,跟踪在使用表单的选项类型?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 T s | vals | the array’s members | T |
[T] | array of T s | keys | the array’s valid indices | Nat |
[var T] | mutable array of T s | vals | the array’s members | T |
[var T] | mutable array of T s | 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 条评论
博客作者 Jose Habel
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.