回想一下,在 Motoko 中,可变(mutable)的状态对于actor来说总是私有(private)的。

但是,两个actor可以共享消息数据,这些消息可以引用(refer to)actor,包括他们自己和彼此。此外,如果这些函数是共享(shared)的,消息可以引用(refer to)单个函数。

通过这些机制,两个actor可以通过异步(asynchronous)消息传递来协调它们的行为。

带有actor的Publisher-subscriber模式:

本节中的示例通过关注publish-subscribe模式的变体,演示actor如何共享其功能。在Publisher-subscriber模式中,一个publishing actor记录subscriber actor列表,以便在publisher的状态中发生值得注意的事情时进行通知。例如,如果publisher actor发布了一篇新文章,则会通知subscriber actor有一篇新文章可用。

下面的示例使用 Motoko 中的两个actor来构建Publisher-subscriber关系的变体。

PS:要查看使用此模式的工作项目的完整代码,请参阅示例存储库中的 pubsub 示例。

Subscriber actor:

下面的 Subscriber actor类型提供了一个可能的接口,以便subscriber actor和publisher actor分别公开和调用:

type Subscriber = actor {
  notify : () -> ()
};
  • Publisher使用此类型定义数据结构以将其Subscribers存储为数据。
  • 每个Subscriber actor公开一个 notify update 函数,如上面的Subscriber actor类型签名中所述。

注意,子类型(sub-typing)允许Subscriber actor包含此类型定义中未列出的其他方法。

为简单起见,假设 notify 函数接受相关的通知数据,并向Publisher返回一些关于Subscriber的新状态消息。例如,Subscriber可能根据通知数据返回对其订阅设置的更改。

Publisher actor:

Publisher端的代码存储Subscribers数组。为简单起见,假设每个Subscriber只使用一个subscribe函数订阅自己一次。

import Array "mo:base/Array";

actor Publisher {
    var subs: [Subscriber] = [];

    public func subscribe(sub: Subscriber) {
        subs := Array.append<Subscriber>(subs, [sub]);
    };

    public func publish() {
        for (sub in subs.vals()) {
          sub.notify();
        };
    };
};

稍后,当某个未指定的外部代理调用publish函数时,所有Subscribers都会收到通知消息,如上面给出的Subscriber类型中所定义的。

Subscriber methods:

在最简单的情况下,Subscriber actor具有以下方法:

  • 使用 init 方法订阅来自Publisher的通知。
  • 按照上面给出的Subscriber类型中的 notify 函数指定的方式,作为其中一个subscribed actor接收通知。
  • 允许对累积状态的查询,在这个示例代码中,它只是一个get方法,用于获取接收到的通知和存储在 count 变量中的通知数。

下面的代码说明了如何实现这些方法:

actor Subscriber {
  var count: Nat = 0;
  public func init() {
    Publisher.subscribe(Subscriber);
  };
  public func notify() {
    count += 1;
  };
  public func get() : async Nat {
    count
  };
}

actor假设只调用其 init 函数一次,但并不强制执行这个函数。在 init 函数中,Subscriber actor传递一个类型为 actor { notify: ()→()} ; (上面在本地,名为Subscriber)的自身的引用。

如果多次调用,actor将多次订阅自己,并从Publisher那里接收多个(重复的)通知。这种脆弱性是我们上面展示的基本Publisher-subscriber模式的结果。如果更加小心,更高级的Publisher actor可以检查重复的Subscriber actor,并忽略他们。

Sharing functions among actors:

在 Motoko 中,共享的actor函数可以在消息中发送给另一个actor,然后由该actor或另一个actor调用。

为了说明起见,对上面显示的代码进行了简化。完整版为Publisher-subscriber关系提供了额外的特性,并使用共享函数使这种关系更加灵活。

例如,通知函数总是被指定为 notify。一个更加灵活的设计将仅仅固定通知的类型,并允许Subscriber选择其共享函数中的任何一个,这些函数在subscribe消息中指定,以代替仅仅包含订阅的actor的消息。

PS:有关详细信息,请参阅完整示例

特别是,假设Subscriber希望避免被锁定到其接口的某个命名方案中。真正重要的是,Publisher可以调用用户选择的某个函数。

shared关键字:

为了允许这种灵活性,actor需要共享一个允许从另一个actor进行远程调用的函数,而不仅仅是对自身的引用。

共享一个函数的能力要求它被预先指定为共享(shared),类型系统强制这些函数遵循这些函数接受、返回和关闭闭包的数据类型的特定规则。

Motoko 允许您省略public actor方法的这个关键字,因为无论是否显式标记,public actor的任何public函数都必须被“共享”。

使用共享函数类型,我们可以扩展上面的例子,使其更加灵活。例如:

type SubscribeMessage = { callback: shared () -> (); };

这个类型与原始类型的不同之处在于,它描述了一个消息记录类型,只有一个名为 callback的字段,上面首先显示的原始类型描述了一个只含有一个名为 notify的方法的actor类型:

type Subscriber = actor { notify : () -> () };

值得注意的是,actor 关键字意味着后一种类型不是包含字段的普通记录,而是包含至少一个“必须名为notify的方法”的 actor。

通过使用 SubscribeMessage 类型,Subscriber actor可以为其 notify方法选择另一个名称:

actor Subscriber {
  var count: Nat = 0;
  public func init() {
    Publisher.subscribe({callback = incr;});
  };
  public func incr() {
    count += 1;
  };
  public query func get(): async Nat {
    count
  };
};

与原始版本相比,唯一更改的行是使用表达式{ callback = incr;}notify 重命名为 incr 并形成新订阅消息有效负载的行。(第四行)

同样地,我们可以更新Publisher以获得一个匹配的接口:

import Array "mo:base/Array";
actor Publisher {
  var subs: [SubscribeMessage] = [];
  public func subscribe(sub: SubscribeMessage) {
    subs := Array.append<SubscribeMessage>(subs, [sub]);
  };
  public func publish() {
    for (sub in subs.vals()) {
      sub.callback();
    };
  };
};
Unknown
- Motoko -

A Student on the way to full stack of Web3.