Using multiple actors

In this tutorial, you are going to create a project with multiple actors. Currently, you can only define one actor in a Motoko file and a single actor is always compiled to a single canister. In addition, you cannot yet define an actor class to support multiple actor instances in your Motoko programs. You can, however, create projects that have multiple actors and can build multiple canisters from the same dfx.json configuration file.

For this tutorial, you are going to create separate program files for three actors in the same project. This project defines the following unrelated actors:

  • The assistant actor provides functions to add and show tasks in a to-do list.

    For simplicity, the code sample for this tutorial only includes the functions to add to-do items and to show the current list of to-do items that have been added. A more complete version of this program-with additional functions for marking items as complete and removing items from the list—is included in Sample code, applications, and microservices.

  • The rock_paper_scissors actor provides a function for determining a winner in a hard-coded rock-paper-scissors contest.

    This code sample illustrates the basic use of switch and case in a Motoko program with hard-coded players and choices.

  • The daemon actor provides mock functions for starting and stopping a daemon.

    This code sample simply assigns a variable and prints messages for demonstration purposes.

Before you begin

Before starting the tutorial, verify the following:

  • You have downloaded and installed the SDK as described in Getting started.

  • You have stopped any network replica processes running on the local computer.

This tutorial takes approximately 20 minutes to complete.

Create a new project

To create a new project for this tutorial:

  1. Open a terminal shell on your local computer, if you don’t already have one open.

  2. Change to the folder you are using for your Internet Computer sample projects.

  3. Create a new project by running the following command:

    dfx new multiple_actors
  4. Change to your project directory by running the following command:

    cd multiple_actors

Modify the default configuration

You have already seen that creating a new project adds a default dfx.json configuration file to your project directory. For this tutorial, you need to add sections to this file to specify the location of each program that defines an actor you want to build.

To modify the default dfx.json configuration file:

  1. Open the dfx.json configuration file in a text editor, then change the default multiple_actors canister name and source directory to assistant.

    For example:

    {
      "canisters": {
        "assistant": {
          "frontend": {
            "entrypoint": "src/multiple_actors/public/index.js"
          },
          "main": "src/assistant/main.mo"
        },

    Because you are going to add settings to this canisters section of the configuration file, you must also add a comma after the curly brace that encloses the location of the assistant main source code file.

  2. Add a new canister name and source file location for the rock_paper_scissors program and a new canister name and source file location for the daemon program files below the assistant source file location.

    For example:

        "rock_paper_scissors": {
          "main": "src/rock_paper_scissors/main.mo"
        },
        "daemon": {
          "main": "src/daemon/main.mo"
    	}

    You can leave the other sections as-is.

  3. Change the name of the default source file directory to match the name specified in the dfx.json configuration file by running the following command:

    cp -r src/multiple_actors/ src/assistant/
  4. Copy the assistant source file directory to create the main program file for the rock_paper_scissors actor by running the following command:

    cp -r src/assistant/ src/rock_paper_scissors/
  5. Copy the assistant source file directory to create the main program file for the daemon actor by running the following command:

    cp -r src/assistant/ src/daemon/

Modify the default template programs

You now have three separate directories in the src directory, each with a template main.mo file. For this tutorial, you will replace the content in each template main.mo file with a different actor.

To modify the default template source code:

  1. Open the src/assistant/main.mo file in a text editor and delete the existing content.

  2. Copy and paste the following sample code into the file:

    import Array "mo:base/Array";
    import Nat "mo:base/Nat";
    
    // Define to-do item properties
    type ToDo = {
    	id: Nat;
    	description: Text;
    	completed: Bool;
    };
    
    // Add to-do item utility
    func add(todos : [ToDo], desc : Text, nextId : Nat) : [ToDo] {
    	let todo : ToDo = {
    		id = nextId;
    		description = desc;
    		completed = false;
    	};
    	Array.append<ToDo>(todos, [todo])
    };
    
    // Show to-do item utility
    func show(todos : [ToDo]) : Text {
    	var output : Text = "\n___TO-DOs___";
    	for (todo : ToDo in todos.vals()) {
    		output #= "\n(" # Nat.toText(todo.id) # ") " # todo.description;
    		if (todo.completed) { output #= " ✔"; };
    	};
    	output
    };
    
    // Define the actor
    actor Assistant {
    
    	var todos : [ToDo] = [];
    	var nextId : Nat = 1;
    
    	public func addTodo (description : Text) : async () {
    		todos := add(todos, description, nextId);
    		nextId += 1;
    	};
    
    	public query func showTodos () : async Text {
    		show(todos)
    	};
    
    };
  3. Open the src/rock_paper_scissors/main.mo file in a text editor and delete the existing content.

  4. Copy and paste the following sample code into the file:

    import I "mo:base/Iter";
    
    actor rock_paper_scissors {
    
    	var alice_score: Nat = 0;
        var bob_score: Nat = 0;
        var alice_last: Choice = { #scissors };
        var bob_last: Choice = { #rock };
    
    	type Choice = {
    		#rock;
    		#paper;
    		#scissors;
    	};
    
        public func contest() : async Text {
    		for (i in I.range(0,99)) {
    			battle_round();
    		};
            var winner = "The contest was a draw";
    		if (alice_score > bob_score) winner := "Alice won"
    		else if (alice_score < bob_score) winner := "Bob won";
            return (winner);
    	};
    
        func battle_round() : () {
         	let a = alice(bob_last);
    	    let b = bob(alice_last);
    
    	  switch (a,b) {
    		case (#rock,#scissors) alice_score +=1;
    		case (#rock,#paper) bob_score +=1;
    		case (#paper,#scissors) alice_score +=1;
    		case (#paper,#rock) bob_score +=1;
    		case (#scissors,#paper) alice_score +=1;
    		case (#scissors,#rock) bob_score +=1;
            case (#rock,#rock) alice_score +=0;
            case (#paper,#paper) bob_score +=0;
            case (#scissors,#scissors) alice_score +=0;
    	  };
    
    	  alice_last := a;
    	  bob_last := b;
    
    	  return ();
    };
          // Hard-coded players and choices
          func bob(last: Choice) : Choice {
        	return #paper;
          };
    
          func alice(last: Choice) : Choice {
        	return #rock;
    	  };
    };
  5. Open the src/daemon/main.mo file in a text editor and delete the existing content.

  6. Copy and paste the following sample code into the file:

    actor daemon {
        var running = false;
    
        public func launch(): async Text {
            running :=  true;
            debug_show "The daemon process is running";
        };
    
        public func stop(): async Text {
            running := false;
            debug_show "The daemon is stopped";
        };
    };

Build all of the canisters in the project

You now have a program that you can compile into an executable WebAssembly module that you can deploy on your local replica network.

To build the executable for each actor in the project:

  1. Change to the ~/ic-projects/multiple_actors root directory for your project, if needed.

  2. Build the WebAssembly executable for each program by running the following command:

    dfx build --skip-frontend

    If the command is successful, it builds all of the canisters you have specified in the dfx.json file.

    Building canister assistant
    Building canister rock_paper_scissors
    Building canister daemon

Deploy the canisters in the project

You now have three separate compiled programs—one for each actor—ready for deployment.

To deploy the canisters:

  1. Start the Internet Computer network on your local computer by running the following command:

    dfx start
  2. Open a new terminal shell, then change the ~/ic-projects/multiple_actors root directory for your project.

    For example:

    cd ~/ic-projects/multiple_actors
  3. Deploy your canisters on the local network by running the following command:

    dfx canister install --all

    The command displays output similar to the following:

    Installing code for canister assistant, with canister_id ic:5C594B7580D80AA71C
    Installing code for canister rock_paper_scissors, with canister_id ic:3A130CE593455AC697
    Installing code for canister daemon, with canister_id ic:3A8E1ED1A2F0B12F4B

Verify deployment by calling functions

You now have three programs deployed as a canisters on your local replica network and can test each program by using dfx canister call commands.

To test the programs you have deployed on the local replica network:

  1. Use the dfx canister call command to call the canister assistant using the addTodo function and pass it the task you want to add by running the following command:

    dfx canister call assistant addTodo '("Schedule monthly demos")'
  2. Verify that the command returns the to-do list item using the showTodos function by running the following command:

    dfx canister call assistant showTodos

    The command returns output similar to the following:

    ("
    ___TO-DOs___
    (1) Schedule monthly demos
  3. Use the dfx canister call command to call the canister rock_paper_scissors using the contest function by running the following command:

    dfx canister call rock_paper_scissors contest

    The command returns the result of the hard-coded contest similar to the following:

    ("Bob won")
  4. Use the dfx canister call command to call the canister daemon using the launch function by running the following command:

    dfx canister call daemon launch
  5. Verify the mock launch function returns "The daemon process is running" message":

    (""The daemon process is running"")
  6. Stop the Internet Computer processes running on your local computer by running the following command:

    dfx stop