Modules and Imports
Modules, like objects, are a collection of named variables, types, functions (and possibly more items) that together serve a special purpose. Modules help us organize our code. A module is usually defined in its own separate source file. It is meant to be imported in a Motoko program from its source file.
Modules are like a limited version of objects. We will discuss their limitations below.
Imports
Lets define a simple module in its own source file module.mo
:
module {
private let x = 0;
public let name = "Peter";
};
This module has two fields, one private, one public. Like in the case of objects, only the public fields are 'visible' from the outside.
Lets now import this module from its source file and use its public field:
// main.mo
import MyModule "module";
let person : Text = MyModule.name;
We use the import
keyword to import the module into our program. Lines starting with import
are only allowed at the top of a Motoko file, before anything else.
We declared a new name MyModule
for our module. And we referenced the module source file module.mo
by including it in double quotes without the .mo
extension.
We then used its public field name
by referencing it through our chosen module name MyModule
. In this case we assigned this name
text value to a newly declared variable person
.
Nested modules
Modules can contain other modules. Lets write a module with two child modules.
module {
public module Person {
public let name = "Peter";
public let age = 20;
};
private module Info {
public let city = "Amsterdam";
};
public let place = Info.city;
};
The top-level module has two named child modules Person
and Info
. The one is public, the other is private.
The public contents of the Info
module are only visible to the top-level module. In this case the public place
variable is assigned the value of the public field city
inside the private Info
child module.
Only the sub-module Person
and variable place
are accessible when imported.
// main.mo
import Mod "module-nested";
let personName = Mod.Person.name;
let city = Mod.place;
Public functions in modules
A module exposes a public API by defining public functions inside the module. In fact, this is what modules are mostly used for. A module with a very simple API could be:
module {
private let MAX_SIZE = 10;
public func checkSize(size : Nat) : Bool {
size <= MAX_SIZE;
};
};
This module has a private constant MAX_SIZE
only available to the module itself. It's a convention to use capital letters for constants.
It also has a public function checkSize
that provides some functionality. It takes an argument and computes an inequality using the private constant and the argument.
We would use this module like this:
// main.mo
import SizeChecker "module-public-functions";
let x = 5;
if (SizeChecker.checkSize(x)) {
// do something
};
We used our newly chosen module name SizeChecker
to reference the public function inside the module and call it with an argument x
from the main file.
The expression SizeChecker.checkSize(x)
evaluates to a Bool
value and thus can be used as an argument in the if expression.
Public types in modules
Modules can also define private and public types. Private types are meant for internal use only, like private variables. Public types in a module are meant to be imported and used elsewhere.
Types are declared using the type
keyword and their visibility is specified:
module {
private type UserData = {
name : Text;
age : Nat;
};
public type User = (Nat, UserData);
};
Only the User
type is visible outside the module. Type UserData
can only be referenced inside a module and even used in a public type (or variable or function) declaration as shown above.
Type imports and renaming
Types are often given a local type alias (renamed) after importing them:
// main.mo
import User "types";
type User = User.User;
In the example above, we first import the module from file types.mo
and give it the module name User
.
Then we define a new type alias also named User
, again using the type
keyword. We reference the imported type by module name and type name: User.User
.
We often use the same name for the module, the type alias and the imported type!
Public classes in modules
Modules can also define private and public classes. Private classes are rarely used internally. Public classes on the other hand are used widely. In fact, most modules define one main class named after the module itself. Public classes in a module are meant to be imported and used elsewhere as 'factories' for objects.
Classes in modules are declared using the class
keyword and their visibility is specified:
module {
public class MyClass(x : Nat) {
private let start = x ** 2;
private var counter = 0;
public func addOne() {
counter += 1;
};
public func getCurrent() : Nat {
counter;
};
};
};
The class MyClass
is visible outside the module. It can only be instantiated if it is imported somewhere else, due to static limitations of modules. Our class takes in one initial argument x : Nat
and defines two internal private variables. It also exposes two public functions.
Class imports and class referencing
Classes in modules are imported from the module source file they are defined in. This file often has the same name as the class! Further more, the module alias locally is also given the same name:
// main.mo
import MyClass "MyClass";
let myClass = MyClass.MyClass(0);
In the example above, we first import the module from source file MyClass.mo
and give it the module name MyClass
.
Then we instantiate the class by referring to it starting with the module name and then the class MyClass.MyClass(0)
. The 0
is the initial argument that our class takes in.
We often use the same name for the module file name, the module local name and the class!
Static expressions only
Modules are limited to static expressions only. This means that no computations can take place inside the module. Static means that no program is running. A module only defines code to be used elsewhere for computations.
A function call is non-static. Computations, like adding and multiplying are also non-static. Therefore the following code is not allowed inside modules:
module {
// public let x = 1 + 1;
// public func compute() {
// 8 - 5
// };
public func compute(x : Nat, y : Nat) : Nat {
x * y;
};
};
The first line in the module tries to compute 1 + 1
which is a 'dynamic' operation. The second line tries to define a function which makes an internal computation, another non-static operation.
The last function compute
is allowed because it only defines how to perform a computation, but does not actually perform the computation. Instead it is meant to be imported somewhere, like in an actor, to perform the computation on any values passed to it.
Module type
Module types are not used often in practice, but they do exist. Modules have types that look almost the same as object types. A type of a module looks like this:
type MyModule = module {
x : Nat;
f : () -> ();
};
This type describes a module with two public fields, one being a Nat
and the other being a function of type () -> ()
.