Actor Classes

In the same way classes are constructor functions for objects, similarly actor classes are constructors for actors. An actor class is like a template for programmatically creating actors of a specific actor type.

But unlike ordinary public classes (that are usually declared inside a module), a single actor class is written in its own separate .mo file and is imported like a module.

See 'Actor classes' and 'Actor classes generalize actors' in the official documentation for more information.

NOTE
For programmatically managing actor classes, also check out Actor class management

A simple actor class

// Actor class in separate source file `actor-class.mo`
actor class User(username : Text) {
    var name = username;

    public query func getName() : async Text { name };

    public func setName(newName : Text) : async () {
        name := newName
    };
};

We use the actor class keywords followed by a name with parentheses User() and an optional input argument list, in this case username : Text.

The body of the actor class, like any actor, may contain variables, private or shared functions, type declarations, private async* functions, etc.

Actor class import

We import the actor class like we would import a module.

// `main.mo`
import User "actor-class";

We import the actor class from actor-class.mo, a file in the same directory. We chose the name User for the module to represent the actor class.

The module type of User now looks like this:

module {
    type User = {
        getName: query () -> async Text;
        setName: Text -> async ();
    };
    User : (Text) -> async User;
};

This module contains two fields:

  1. The type of the actor that we can 'spin up' with this actor class.
    In this case, the actor type User consists of two shared functions, one of which is a query function.

  2. The constructor function that creates an actor of this type.
    The function User takes a Text argument and returns a future async User for an actor of type User.

NOTE
In the module above, the name User is used as the name of a type and a function, see imports. The line User : (Text) -> async User; first uses the name User as function name and then as a type name in async User.

Installing an instance of an actor class

The function User : (Text) -> async User can be called and awaited from an asynchronous context from within a running actor. Lets refer to this actor as the Parent actor.

The await for User initiates an install of a new instance of the actor class as a new 'Child' actor running on the IC.

let instance = await User.User("Alice");

A new canister is created with the Parent actor as the single controller of the new canister.

The await yields an actor actor {} with actor type User. This actor can be stored locally in the Parent actor and used as a reference to interact with the Child actor.

Multi-canister scaling

Whenever you need to scale up your application to multiple actors (running in multiple canisters), you could use actor classes to repeatedly install new instances of the same actor class.

// `main.mo`
import Principal "mo:base/Principal";
import Buffer "mo:base/Buffer";

import User "actor-class";

actor {
    let users = Buffer.Buffer<User.User>(1);

    public func newUser(name : Text) : async Principal {
        let instance = await User.User(name);
        users.add(instance);

        Principal.fromActor(instance);
    };
};

This actor declares a Buffer of type Buffer<User.User> with User.User (our actor type from our actor class module) as a generic type parameter. The buffer is named users and has initial capacity of 1. We can use this buffer to store instances of newly created actors from our actor class.

The shared function newUser takes a Text and uses that as the argument to await the function User.User. This yields a new actor named instance.

We add the new actor to the buffer (users.add(instance)) to be able to interact with it later.

Finally, we return the principal of the new actor by calling Principal.fromActor(instance).

NOTE
On the IC we actually need to provide some cycles with the call to the actor constructor User.User(). On Motoko Playground, this code may work fine for testing purposes.

Calling child actors

The last example is not very useful in practice, because we can't interact with the actors after they are installed. Lets add some functionality that allows us to call the shared functions of our Child actors.

// `main.mo`
import Principal "mo:base/Principal";
import Buffer "mo:base/Buffer";
import Error "mo:base/Error";

import User "actor-class";

actor {
    let users = Buffer.Buffer<User.User>(1);

    public func newUser(name : Text) : async Principal {
        let instance = await User.User(name);
        users.add(instance);

        Principal.fromActor(instance);
    };

    public func readName(index : Nat) : async Text {
        switch (users.getOpt(index)) {
            case (?user) { await user.getName() };
            case (null) { throw (Error.reject "No user at index") };
        };
    };

    public func writeName(index : Nat, newName : Text) : async () {
        switch (users.getOpt(index)) {
            case (?user) { await user.setName(newName) };
            case (null) { throw (Error.reject "No user at index") };
        };
    };
};

We added two shared functions readName and writeName. They both take a Nat to index into the users buffer. They use the getOpt function ('method' from the Buffer Class) in a switch expression to test whether an actor exists at that index in the buffer.

If an actor exists at that index, we bind the name user to the actor instance and so we can call and await the shared functions of the Child actor by referring to them like user.getName(). Otherwise, we throw an Error.