2021.5.3

Motoko笔记
2021050315014715-1024x433.jpeg

Internet Computer的编程模型由内存隔离容器组成,这些容器通过二进制数据编码的异步消息传递进行通信。容器一次处理一条消息,防止出现竞争情况。容器使用回调来注册需要对其发出的任何容器间消息的结果执行的操作。

Motoko用一个众所周知的更高层次的抽象:actor模型来抽象互联网计算机的复杂性。每个容器都表示为一个类型化的参与者。参与者的类型列出了它可以处理的消息。每个消息都被抽象为一个类型化的异步函数。从actor类型到Candid类型的转换对底层Internet Computer的原始二进制数据施加了结构。参与者与对象类似,但不同之处在于,它的状态是完全隔离的,它与世界的交互完全通过异步消息传递,它的消息一次处理一次,即使是由并发参与者并行发出。

在Motoko中,向参与者发送消息是一个函数调用,但不是在调用返回之前阻止调用者,而是将消息排入被调用者的队列,并将表示该挂起请求的未来立即返回给调用者。future是请求最终结果的占位符,调用者可以稍后查询。在发出请求和决定等待结果之间,调用者可以自由地执行其他工作,包括向相同或其他参与者发出更多请求。一旦调用者处理了请求,future就完成了,其结果可供被调用者使用。如果被调用方正在等待将来,则其执行可以随结果一起继续,否则结果只会存储在将来供以后使用。

在Motoko中,actor有专门的语法和类型;消息传递由所谓的返回future的共享函数来处理(共享是因为它们对远程参与者可用);a future,f,是某种类型T的特殊类型async T的值;等待f完成表示为await f,用来获得T类型的值。为了避免通过消息传递引入共享状态,例如,通过发送对象或可变数组,可以将共享函数传输的数据被限制为不可变的共享类型。

首先,我们考虑最简单的有状态服务:Counter actor,即我们以前的本地Counter对象的分布式版本。

1.【Example: a Counter service】

actor Counter { 
	var count = 0; 
	public shared func inc() : async () { count += 1 }; 
	public shared func read() : async Nat { count }; 
	public shared func bump() : async Nat { count += 1; count; }; 
};

The Counter actor declares one field and three public, shared functions:

  • the field count is mutable, initialized to zero and implicitly private.
  • function inc() asynchronously increments the counter and returns a future of type async () for synchronization.“函数inc()异步递增counter,并返回类型为async()的future进行同步。”
  • function read() asynchronously reads the counter value and returns a future of type async Nat containing its value.“函数read()异步读取counter值,并返回包含其值的类型为async Nat的future。”
  • function bump() asynchronously increments and reads the counter.“函数bump()异步递增并读取counter。”

与本地函数不同,共享函数对远程调用方是可访问的,并且具有附加的限制:它们的参数和返回值必须是共享类型—包括不可变数据(immutable data)、actor引用和共享函数引用(shared function references)的类型子集,但不包括对本地函数和可变数据的引用。因为与actor的所有交互都是异步的,所以actor的函数必须返回future,即,对于某些类型T,返回形式为async T的类型。

The only way to read or modify the state (count) of the Counter actor is through its shared functions.

A value of type async T is a future. The producer of the future completes the future when it returns a result, either a value or error.

与对象和模块不同,actor只能公开函数,这些函数必须共享(shared)。因此,Motoko允许您省略公共actor函数前的shared修饰符,允许更简洁但等效的actor声明:

actor Counter { 
	var count = 0; 
	public func inc() : async () { count += 1 }; 
	public func read() : async Nat { count }; 
	public func bump() : async Nat { count += 1; count; }; 
};

结果:

{bump = func; inc = func; read = func} :
	actor {
		bump : shared () -> async Nat;
		inc : shared () -> async ();
		read : shared () -> async Nat
}

目前,共享函数只能声明在actor或actor类的主体中。尽管有这个限制,共享函数仍然是Motoko中的一级(first-class)值,可以作为参数或结果传递,并存储在数据结构中。

共享函数的类型是使用共享函数类型时指定的。例如,值inc具有类型shared()→ async Nat,可以作为独立回调提供给其他服务(请参见publish-subscribe以获取示例)。

2.【Actor Types】

就像objects有object types一样,actors也有actor types。

如1.中的Counter actor有以下的type:

actor { 
	inc : shared () -> async (); 
	read : shared () -> async Nat; 
	bump : shared () -> async Nat; 
}

同样,由于actor的每个成员都需要shared修饰符,Motoko在显示时都省略了它们,并且允许您在编写actor类型时省略它们。

因此,前面的类型可以更简洁地表示为:

actor { 
	inc : () -> async (); 
	read : () -> async Nat; 
	bump : () -> async Nat; 
}

与对象类型一样,actor类型支持子类型:actor类型是更通用的类型的子类型,它(指更通用的类型)提供的函数更少,类型更通用。

3.【Using await to consume async futures】

The caller of a shared function typically receives a future, a value of type async Tfor some T.

The only thing the caller, a consumer, can do with this future is wait for it to be completed by the producer, throw it away, or store it for later use.

要访问async 值的结果,future的接受者要使用await表达式。

例如,要使用上面Counter.read()的结果,我们可以将future绑定到标识符a,然后await a检索底层Nat,n:

let a : async Nat = Counter.read(); 
let n : Nat = await a;

通常,将这两个步骤合并为一个步骤,一个步骤直接等待异步调用:

let n : Nat = await Counter.read();

⚠️Counter.read()的返回值类型为async Nat,可以发现,规则是:

「await 作为广义上的左值,要接受一个async _ 作为右值」

与本地函数调用不同,本地函数调用会在被调用方返回结果之前阻止调用方,而共享函数调用会立即返回future f,而不会阻止。也因此,稍后调用 await f 时将挂起当前计算,直到 f 完成。一旦future完成(由producer),wait p的执行将恢复并带来其结果。如果结果是一个值,那么 await f 会返回该值。否则结果是一些错误,并且 await f 会将错误传播给await f 的使用者。

Awaiting a future a second time will just produce the same result, including re-throwing any error stored in the future. Suspension occurs even if the future is already complete; this ensures state changes and message sends prior to every await are committed.

[WARNING]

A function that does not await in its body is guaranteed to execute atomically - in particular, the environment cannot change the state of the actor while the function is executing. If a function performs an await, however, atomicity is no longer guaranteed. 在等待前后的暂停和恢复之间,由于并发处理其他传入的actor消息,封闭actor的状态可能会改变。程序员有责任防止不同步的状态变化。然而,程序员可以依赖于在提交await之前的任何状态更改。

例如,上面bump()的实现保证在一个原子步骤(atomic step)中递增并读取count的值。

而以下方案:

public shared func bump() : async Nat { 
	await inc(); 
	await read(); 
};

不具有相同的语义,并允许actor的另一个客户机干扰其操作:每个等待暂停执行,允许入侵者更改actor的状态。通过设计,明确的await使潜在的干扰点清晰地呈现给读者。

4.【Traps and Commit Points】

A trap是一种不可恢复的运行时故障,例如,由零除、数组索引越界、数值溢出、循环耗尽或断言故障引起。

不执行 await 表达式的共享函数调用从不挂起并以原子方式执行。不包含 await 表达式的共享函数在语法上是原子的(atomic)。

一种原子共享函数,其执行陷阱对封闭参与者或其环境的状态没有明显影响—任何状态更改都将被还原,它发送的任何消息都将被撤销。事实上,所有状态更改和消息发送在执行期间都是暂时的:它们只有在到达成功的提交点之后才提交。

The points at which tentative state changes and message sends are irrevocably committed are:

  • implicit(隐式) exit from a shared function by producing a result,
  • explicit(显式) exit via return or throw expressions, and
  • explicit await expressions.

(执行了以上三点的语句以后,之前的所有状态更改和消息发送均无法恢复,效果均已提交)

A trap只会撤消自上次提交点以来所做的更改。特别是,在执行多次 await 的非原子函数中,陷阱只会撤消自上次await 以来尝试的更改—所有之前的效果都已提交,无法撤消。

For example, consider the following (contrived) stateful Atomicity actor:

(不清楚的请认真阅读以下程序和分析)

actor Atomicity {
	var s = 0; 
	var pinged = false; 
	public func ping() : async () { 
		pinged := true; 
	}; 

	// an atomic method 
	public func atomic() : async () { 
		s := 1; 
		ignore ping(); 
		ignore 0/0; // trap! 
	};

	// a non-atomic method 
	public func nonAtomic() : async () { 
		s := 1; 
		let f = ping(); 
		s := 2; 
		await f; 
		s := 3; // this will not be rolled back! 
		await f;      //会重新调用ping(),f的实质不是ping()的返回值,而是类似于函数的调用接口
		ignore 0/0;// trap! 
	}; 
};

分析:

Calling (shared) function atomic() will fail with an error, since the last statement causes a trap. 

然而,atomic()函数被调用之后,s还是0而不是1,pinged还是false而不是true。This is because the trap happens before method atomic has executed an await, or exited with a result. Even though atomic calls ping(), ping() is tentative (queued) until the next commit point, so never delivered.

Calling (shared) function nonAtomic() will fail with an error, since the last statement causes a trap. 

然而,nonAtomic()调用之后,s变成了3而不是0,pinged变成了true而不是false。 This is because each await commits its preceding side-effects, including message sends. Even though f is complete by the second await on f, this await also forces a commit of the state, suspends execution and allows for interleaved processing of other messages to this actor.

5.【Query functions】

在Internet计算机术语中,以上所有三个Counter函数都是更新消息,在调用时可以改变容器的状态。实现状态更改需要在分布式副本之间达成一致,然后Internet计算机才能提交更改并返回结果。达成共识是一个代价昂贵的过程,延迟相对较高。

对于不需要一致性保证的应用程序部分,Internet Computer支持更高效的查询操作。它们能够从单个副本读取容器的状态,在执行期间修改快照并返回结果,但不能永久更改状态或发送更多Internet Computer消息。

Motoko支持使用query查询函数实现Internet Computer查询。query关键字修改(共享)actor函数的声明,使其以非提交的、更快的Internet Computer查询语义执行。

For example, we can extend the Counter actor with a fast-and-loose variant of the trustworthy read function, called peek:

actor Counter { 
	var count = 0; 
	// ... 
	public shared query func peek() : async Nat { count }; 
}

Counter前端可以使用peek()函数快速显示当前count值,但不太可靠。(因为没有经过共识)

在query方法中(query函数体中)调用actor函数是一个编译时错误,因为这将违反Internet计算机施加的动态限制。只允许调用普通函数。

可以从非查询函数调用查询函数。因为这些嵌套调用需要一致性,所以嵌套查询调用的效率收益最多只能算是适度的。

query修饰符反映在query函数的type中是这样的:

peek : shared query () -> async Nat

与前面一样,在query声明和actor类型中,可以省略shared关键字。

6.【Messaging Restrictions】

Internet Computer对何时以及如何允许容器通信进行了限制。这些限制在Internet Computer上动态执行,但在Motoko中静态阻止,排除了一类动态执行错误。两个例子是:

  • canister installation can execute code, but not send messages.
  • a canister query method cannot send messages.

在Motoko中,这些限制作为对某些表达式可以使用的上下文的限制出现。

在Motoko中,如果一个表达式出现在异步表达式的主体中,那么它就出现在异步上下文中,异步表达式可以是(共享的或本地的)函数的主体,也可以是独立的表达式。唯一的例外是查询函数,它的主体不被认为是开启异步上下文的。

在Motoko中,调用共享函数是错误的,除非该函数是在异步上下文中调用的。此外,在actor类的构造函数中调用共享函数也是一个错误。

The await construct is only allowed in an asynchronous(异步的)context.

The async construct is only allowed in an asynchronous(异步的)context.

只能在异步上下文中抛出或尝试/捕获错误。这是因为结构化错误处理仅支持消息传递错误,并且与消息传递本身一样,仅限于异步上下文。

这些规则还意味着本地函数通常不能直接调用共享函数或等待未来。这种限制有时会很尴尬:我们希望在将来扩展类型系统,使其更为宽松。

7.【Actor classes generalize actors】

An actor class 将单个actor声明泛化为满足相同接口的actor家族的声明。 An actor class declares a type, naming the interface of its actors, and a function that constructs a fresh actor of that type each time it is supplied with an argument.An actor class thus serves as a factory for manufacturing actors. Because canister installation is asynchronous on the Internet Computer, the constructor function is asynchronous too, and returns its actor in a future.“actor class声明一个type,命名其actor的接口,以及一个函数,该函数在每次提供参数时构造该类型的新actor。因此,actor class充当制造actor的工厂。因为容器安装在Internet计算机上是异步的,所以构造函数也是异步的,并在将来返回其参与者。”

For example, we can generalize Counter given above to Counter(init) below, by introducing(传入) a constructor parameter, variable init of type Nat:

actor class Counter(init : Nat) { 
	var count = init; 
	public func inc() : async () { count += 1 }; 
	public func read() : async Nat { count }; 
	public func bump() : async Nat { count += 1; count; }; 
};

If this class is stored in file Counters.mo, then we can import the file as a module(模板) and use it to create several actors with different initial values:

import Counters "Counters"; 
let C1 = await Counters.Counter(1); 
let C2 = await Counters.Counter(2); 
(await C1.read(), await C2.read())      //把输出表达式格式化了一下

结果:

(1, 2) : (Nat, Nat)

上面的最后两行实例化了actor类两次。第一次调用使用初始值1,第二次调用使用初始值2。因为actor类实例化是异步的,所以每个对Counter的调用(init)都会返回一个未来,可以等待生成的actor值。C1和C2都有相同的类型Counters.Counter,并且可以互换使用。

Notes:

目前,Motoko编译器在编译不是由单个actor或actor类组成的程序时会给出一个错误。然而,编译的程序仍然可能引用导入的actor类。有关详细信息,请参见 Importing actor classesActor classes


A Student on the way to full stack of Web3.