1.11「Motoko——Sharing data and behavior」

1.11「Motoko——Sharing data and behavior」

Recall that in Motoko mutable state is always private to actors.

However, two actors can share message data which can refer to actors, both themselves and each other. Additionally, messages can refer to individual functions if those functions are shared .

Through these mechanisms, two actors can coordinate their behavior through asynchronous message passing.

Publisher-subscriber pattern with actors:

The examples in this section demonstrate how actors can share their functionality by focusing on variants of the publish-subscribe pattern. In the Publisher-subscriber pattern, a publishing actor keeps track of a list of subscriber actors to be notified when something noteworthy happens in the publisher 's state. For example, if the publisher actor publishes a new article, the subscriber actor is notified that a new article is available.

The example below uses two actors in Motoko to build a variant of the Publisher-subscriber relationship.

PS: To see the full code for a working project using this pattern, see the pubsub example in the examples repository.

Subscriber actor:

The following Subscriber actor types provide a possible interface for subscriber and publisher actors to expose and call respectively:

type Subscriber = actor {
    
    
  notify : () -> ()
};
  • Publishers use this type to define data structures to store their Subscribers as data.
  • Each Subscriber actor exposes a notify update function, as described in the Subscriber actor type signature above .

Note that subtyping allows Subscriber actors to contain additional methods not listed in this type definition.

For simplicity, assume that the notify function accepts relevant notification data and returns some new status messages about the Subscriber to the Publisher . For example, a Subscriber might return changes to its subscription settings based on notification data.

Publisher actor:

The code on the Publisher side stores the Subscribers array. For simplicity, assume that each Subscriber only subscribes itself once using a subscribe function.

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();
        };
    };
};

Later, when some unspecified external proxy calls the publish function, all Subscribers will receive a notification message, as defined in the Subscriber type given above .

Subscriber methods:

In the simplest case, the Subscriber actor has the following methods:

  • Subscribe to notifications from the Publisher using the init method .
  • Receive notifications as one of the subscribed actors as specified by the notify function in the Subscriber type given above .
  • Allows querying of the cumulative state, which in this sample code is just a get method for the notifications received and the number of notifications stored in the count variable.

The following code illustrates how to implement these methods:

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

The actor assumes that its init function is called only once, but does not enforce this function. In the init function, the Subscriber actor is passed a reference to itself of type actor { notify: ()→()} ;(above local, named ).Subscriber

If called multiple times, the actor will subscribe itself multiple times and receive multiple (duplicate) notifications from the Publisher . This vulnerability is a consequence of the basic Publisher-subscriber pattern we showed above. With more care, a more advanced Publisher actor could check for duplicate Subscriber actors, and ignore them.

Sharing functions among actors:

In Motoko, shared actor functions can be sent in messages to another actor, and then called by that actor or another actor.

The code shown above has been simplified for illustration. The full version provides additional features for the Publisher-subscriber relationship and uses shared functions to make this relationship more flexible.

For example, a notification function is always specified as notify . A more flexible design would just fix the type of notification and allow the Subscriber to choose any of its shared functions, specified in the subscribe message, instead of messages containing only subscribed actors.

PS: See full example for details .

In particular, suppose Subscriber wishes to avoid being locked into a certain naming scheme for its interfaces. What really matters is that the Publisher can call some function of the user's choosing.

shared keyword:

To allow this flexibility, actors need to share a function that allows remote calls from another actor, not just a reference to themselves.

The ability to share a function requires that it be pre-specified as shared , and the type system enforces that these functions follow specific rules for the data types that these functions accept, return, and close closures on.

Motoko allows you to omit this keyword for public*** actor methods, since any public * functions of a public actor must be "shared" , whether explicitly marked or not .

Using shared function types, we can extend the above example to be more flexible. For example:

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

This type differs from the primitive type in that it describes a message record type with only one field named callback , whereas the primitive type shown first above describes an actor type with only one method named notify :

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

It is worth noting that the actor keyword means that the latter type is not a normal record containing fields, but an actor containing at least one "method which must be named notify ".

By using the SubscribeMessage type, the Subscriber actor can choose another name for its notify method:

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
  };
};

Compared to the original version, the only changed line is the one that uses the expression { callback = incr;}to rename notify to incr and form the new subscription message payload. (fourth row)

Likewise, we can update the Publisher to have a matching interface:

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-

Guess you like

Origin blog.csdn.net/qq_29810031/article/details/123248186