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:
-
The type of the actor that we can 'spin up' with this actor class.
In this case, the actor typeUser
consists of two shared functions, one of which is a query function. -
The constructor function that creates an actor of this type.
The functionUser
takes aText
argument and returns a futureasync User
for an actor of typeUser
.
NOTE
In the module above, the nameUser
is used as the name of a type and a function, see imports. The lineUser : (Text) -> async User;
first uses the nameUser
as function name and then as a type name inasync User
.
Installing an instance of an actor class
The function User : (Text) -> async User
can be called and await
ed 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 constructorUser.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.