Sharing data and behavior

Recall that in Motoko, mutable state is always private to an actor.

However, two actors can share message data, and those messages can refer to actors, including themselves and one another. 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 illustrate how actors share their functions by focusing on variations of the publish-subscribe pattern. In the publish-subscribe pattern, a publishing actor records a list of subscriber actors to notify when something notable occurs in the publisher’s state. For example, if the publisher actor publishes a new article, the subscriber actors are notified that a new article is available.

The example below uses two actors in Motoko to build variations of the publisher-subscriber relationship.

To see the complete code for a working project that uses this pattern, see the pubsub example in the examples repository.

Subscriber actor

The following Subscriber actor type provides a possible interface for the subscriber actor and the publisher actor to expose and to call, respectively:

public type Subscriber = actor {
  notify : () -> ()
};
  • The Publisher uses this type to define a data structure to store its subscribers as data.

  • Each Subscriber actor exposes a notify update function as described in the Subscriber actor type signature above.

Note that sub-typing enables the Subscriber actor to include additional methods that are not listed in this type definition.

For simplicity, assume that the notify function accepts relevant notification data and returns some new status message about the subscriber to the publisher. For example, the subscriber might return a change to its subscription settings based on the notification data.

Publisher actor

The publisher side of the code stores an array of subscribers. For simplicity, assume that each subscriber only subscribes itself once using a subscribe function.

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 agent invokes the publish function, all of the subscribers receive the notify 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 notification as one of the subscribed actors as specified by the notify function in the Subscriber type given above).

  • Permit queries to the accumulated state, which in this sample code is simply a get method for the number of notifications received and stored in the count variable.

The following code illustrates implementing these methods:

import Publisher "canister:pub";

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, but does not enforce, that its init function is only ever called once. In the init function, the Subscriber actor passes a reference to itself, of type actor { notify : () → () }; (locally called Subscriber above).

If called more than once, the actor will subscribe itself multiple times, and will receive multiple (duplicate) notifications from the publisher. This fragility is the consequence of the basic publiisher-subscriber design we show above. With more care, a more advanced publisher actor could check for duplicate subscriber actors and ignore them, for instance.

Sharing functions among actors

In Motoko, a shared actor function can be sent in a message to another actor, and then later called by that actor, or by another actor.

The code shown above has been simplified for illustrative purposes. The full version offers additional features to the publisher-subscriber relationship, and uses shared functions to make this relationship more flexible.

For instance, the notification function is always designated as notify. A more flexible design would only fix the type of notify, and permit the subscriber to choose any of its shared functions, specified in a subscribe message in place of (just) the actor that is subscribing.

See the the full example for details.

In particular, suppose that the subscriber wants to avoid being locked into a certain naming scheme for its interface. What really matters is that the publisher can call some function that the subscriber chooses.

The shared keyword

To permit this flexibility, an actor needs to share a single function that permits remote invocation from another actor, not merely a reference to itself.

The ability to share a function requires that it be pre-designated as shared, and the type system enforces that these functions follow certain rules around the types of data that these functions accept, return, and over which their closures close.

We omit this keyword for actor methods since implicitly, all public functions of every actor are `shared`, whether marked explicitly or not.

More generally, a shared function is one that is either part of the public interface of an actor, or it is not mentioned in the public interface, but it does not close over the actor’s mutable state, and it adheres to the same argument and return-type typing restrictions as a public actor function (that is, no mutable data in the arguments or results).

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

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

This type differs from the original, in that it describes a message record type with a single field called callback, and the original type first shown above describes an actor type with a single method called notify:

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

Notably, the actor keyword means that this latter type is not an ordinary record with fields, but rather, an actor with at least one method, which must be called notify.

By using the SubscribeMessage type instead, the Subscriber actor can choose another name for their 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 lines that change are those that rename notify to incr, and form the new subscribe message payload, using the expression {callback = incr}.

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

type SubscribeMessage = { callback: shared () -> (); };
actor Publisher {
    var subs: [SubscribeMessage] = [];
    public func subscribe(sub: SubscribeMessage) {
         subs := Array.append<SubscribeMessage>(subs, [sub]);
    };
    public func publish() {
        for (sub in subs.vals()) {
            subscriber.callback();
         };
    };
};