Skip to main content

Local objects and classes

In Motoko, an object may encapsulate local state (var-bound variables) by packaging this state with public methods that access and update it.

As in other typed languages, Motoko programs benefit from the ability to encapsulate state as objects with abstract types.

However, Motoko objects that include mutable state are not shareable, and this is a critical security-oriented design decision.

If they were shareable, that would mean either conceptually moving a mobile object’s code among actors and executing it remotely, a security risk, or sharing state with remote logic, another security risk. (Notably, as a subcase, objects may be pure records and those are shareable, since they are free from mutable state.)

To compensate for this necessary limitation, actor objects are shareable, but always execute remotely. They communicate with shareable Motoko data only. Local objects interact in less restricted ways with themselves, and can pass any Motoko data to each other’s methods, including other objects. In most other ways, local objects (and classes) are non-shareable counterparts to actor objects (and classes).

The Mutable state introduced declarations of private mutable state, in the form of var-bound variables and (mutable) array allocation. In this chapter, we use mutable state to implement simple objects, much like how we would implement simple objects in object-oriented programming.

We illustrate this support via a running example, which continues in the next chapter. The following example illustrates a general evolution path for Motoko programs. Each object, if important enough, has the potential to be refactored into an Internet service, by refactoring this (local) object into an actor object.

Object classes. Frequently, one needs a family of related objects to perform a task. When objects exhibit similar behavior, it makes sense to fabricate them according to the same blueprint, but with customizable initial state. To this end, Motoko provides a syntactical construct, called a class definition, which simplifies building objects of the same type and implementation. We introduce these after discussing objects.

Actor classes. When an object class exposes a service (asynchronous behavior), the corresponding Motoko construct is an actor class, which follows a similar (but distinct) design.

Example: The counter object

Consider the following object declaration of the object value counter:

object counter {
var count = 0;
public func inc() { count += 1 };
public func read() : Nat { count };
public func bump() : Nat {
inc();
read()
};
};

This declaration introduces a single object instance named counter, whose entire implementation is given above.

In this example, the developer exposes three public functions inc, read and bump using keyword public to declare each in the object body. The body of the object, like a block expression, consists of a list of declarations.

In addition to these three functions, the object has one (private) mutable variable count, which holds the current count, initially zero.

Object types

This object counter has the following object type, written as a list of field-type pairs, enclosed in braces ({ and }):

{
inc : () -> () ;
read : () -> Nat ;
bump : () -> Nat ;
}

Each field type consists of an identifier, a colon :, and a type for the field content. Here, each field is a function, and thus has an arrow type form (_ → _).

In the declaration of object, the variable count was explicitly declared neither as public nor as private.

By default, all declarations in an object block are private, as is count here. Consequently, the type for count does not appear in the type of the object, and its name and presence are both inaccessible from the outside.

The inaccessibility of this field comes with a powerful benefit: By not exposing this implementation detail, the object has a more general type (fewer fields), and as a result, is interchangeable with objects that implement the same counter object type differently, without using such a field.

Example: The byteCounter object

To illustrate the point just above, consider this variation of the counter declaration above, of byteCounter:

import Nat8 "mo:base/Nat8";
object byteCounter {
var count : Nat8 = 0;
public func inc() { count += 1 };
public func read() : Nat { Nat8.toNat(count) };
public func bump() : Nat { inc(); read() };
};

This object has the same type as the previous one, and thus from the standpoint of type checking, this object is interchangeable with the prior one:

{
inc : () -> () ;
read : () -> Nat ;
bump : () -> Nat ;
}

Unlike the first version, however, this version does not use the same implementation of the counter field. Rather than use an ordinary natural Nat that never overflows, but may also grow without bound, this version uses a byte-sized natural number (type Nat8) whose size is always eight bits.

As such, the inc operation may fail with an overflow for this object, but never the prior one, which may instead (eventually) fill the program’s memory, a different kind of application failure.

Neither implementation of a counter comes without some complexity, but in this case, they share a common type.

In general, a common type shared among two implementations (of an object or service) affords the potential for the internal implementation complexity to be factored away from the rest of the application that uses it. Here, the common type abstracts over the simple choice of a number’s representation. In general, the implementation choices would each be more complex, and more interesting.

Object subtyping

To illustrate the role and use of object subtyping in Motoko, consider implementing a simpler counter with a more general type (fewer public operations):

object bumpCounter {
var c = 0;
public func bump() : Nat {
c += 1;
c
};
};

The object bumpCounter has the following object type, exposing exactly one operation, bump:

{
bump : () -> Nat ;
}

This type exposes the most common operation, and one that only permits certain behavior. For instance, the counter can only ever increase, and can never decrease or be set to an arbitrary value.

In other parts of a system, we may in fact implement and use a less general version, with more operations:

fullCounter : {
inc : () -> () ;
read : () -> Nat ;
bump : () -> Nat ;
write : Nat -> () ;
}

Here, we consider a counter named fullCounter with a less general type than any given above. In addition to inc, read and bump, it additionally includes write, which permits the caller to change the current count value to an arbitrary one, such as back to 0.

Object subtyping. In Motoko, objects have types that may relate by subtyping, as the various types of counters do above. As is standard, types with more fields are less general (are *subtypes of) types with fewer fields*. For instance, we can summarize the types given in the examples above as being related in the following subtyping order:

  • Most general:
{ bump : () -> Nat }
  • Middle generality:
{
inc : () -> () ;
read : () -> Nat ;
bump : () -> Nat ;
}
  • Least generality:
{
inc : () -> () ;
read : () -> Nat ;
bump : () -> Nat ;
write : Nat -> () ;
}

If a function expects to receive an object of the first type ({ bump: () → Nat }), any of the types given above will suffice, since they are each equal to, or a subtype of, this (most general) type.

However, if a function expects to receive an object of the last, least general type, the other two will not suffice, since they each lack the needed write operation, to which this function rightfully expects to have access.

Object classes

In Motoko, an object encapsulates state, and an object class is a package of two entities that share a common name.

Consider this example class for counters that start at zero:

class Counter() {
var c = 0;
public func inc() : Nat {
c += 1;
return c;
}
};

The value of this definition is that we can construct new counters, each starting with their own unique state, initially at zero:

let c1 = Counter();
let c2 = Counter();

Each is independent:

let x = c1.inc();
let y = c2.inc();
(x, y)

We could achieve the same results by writing a function that returns an object:

func Counter() : { inc : () -> Nat } =
object {
var c = 0;
public func inc() : Nat { c += 1; c }
};

Notice the return type of this constructor function (an object type):

{ inc : () -> Nat }

We may want to name this type, for example, Counter, as follows, for use in further type declarations:

type Counter = { inc : () -> Nat };

In fact, the class keyword syntax shown above is nothing but a shorthand for these two definitions for Counter: a factory function Counter that constructs objects, and the type Counter of these objects. Classes do not provide any new functionality beyond this convenience.

Class constructor

An object class defines a constructor function that may carry zero or more data arguments and zero or more type arguments.

The Counter example above has zero of each.

The type arguments, if any, parameterize both the type and the constructor function for the class.

The data arguments, if any, parameterize (only) the constructor function for the class.

Data arguments

Suppose we want to initialize the counter with some non-zero value. We can supply that value as a data argument to the class constructor:

class Counter(init : Nat) {
var c = init;
public func inc() : Nat { c += 1; c };
};

This parameter is available to all methods.

For instance, we can reset the Counter to its initial value, a parameter:

class Counter(init : Nat) {
var c = init;
public func inc() : Nat { c += 1; c };
public func reset() { c := init };
};

Type arguments

Suppose we want the counter to actually carry data that it counts (like a specialized Buffer).

When classes use or contain data of arbitrary type, they carry a type argument (or equivalently, type parameter) for that unknown type, just as with functions.

The scope of this type parameter covers the entire class, just as with data parameters. As such, the methods of the class can use (and need not re-introduce) these type parameters.

import Buffer "mo:base/Buffer";

class Counter<X>(init : Buffer.Buffer<X>) {
var buffer = init.clone();
public func add(x : X) : Nat {
buffer.add(x);
buffer.size()
};

public func reset() {
buffer := init.clone()
};
};

Type annotation

Optionally, the class constructor may also carry a type annotation for its "return type" (the type of objects that it produces). When supplied, Motoko checks that this type annotation is compatible with the body of the class (an object definition). This check ensures that each object produced by the constructor meets the supplied specification.

For example, we repeat the Counter as a buffer, and annotate it with a more general type Accum<X> that permits adding, but not resetting the counter. This annotation ensures that the objects are compatible with the type Accum<X>.

import Buffer "mo:base/Buffer";

type Accum<X> = { add : X -> Nat };

class Counter<X>(init : Buffer.Buffer<X>) : Accum<X> {
var buffer = init.clone();
public func add(x : X) : Nat { buffer.add(x); buffer.size() };
public func reset() { buffer := init.clone() };
};

Full syntax

In full, classes are defined by the keyword class, followed by: - a name for the constructor and type being defined (for example, Counter) - optional type arguments (for example, omitted, or <X>, or <X, Y>) - an argument list (for example, (), or (init : Nat), etc.) - an optional type annotation for the constructed objects (for example, omitted, or Accum<X>), - the class "body" is an object definition, parameterized by the type and value arguments (if any).

The constituents of the body marked public contribute to the resulting objects' type and these types compared against the (optional) annotation, if given.

Another example: Bits

As another example, let’s consider the task of walking the bits of a natural number (type Nat). For this example, we could define the following:

class Bits(n : Nat) {
var state = n;
public func next() : ?Bool {
if (state == 0) { return null };
let prev = state;
state /= 2;
?(state * 2 != prev)
}
}

The above class definition is equivalent to the simultaneous definition of a structural type synonym and a factory function, both named Bits:

type Bits = {next : () -> ?Bool};
func Bits(n : Nat) : Bits = object {
// class body
};

Structural subtyping

Object subtyping in Motoko uses structural subtyping, not nominal subtyping.

Recall that in nominal typing, the question of two types equality depends on choosing consistent, globally-unique type names (across projects and time).

In Motoko, the question of two types' equality is based on their structure, not their names.

Due to structural typing, naming the class type provides a convenient abbreviation.

For typing purposes, however, all that matters is the structure of the corresponding object type: two classes with different names but equivalent definitions produce type-compatible objects.

When the optional type annotation is supplied in a class declaration, conformance is checked: the object type must be a subtype of the annotation. The annotation does not affect the type of the class, however, even if it only describes a proper super-type of the object type.

Formally, subtyping relationships in Motoko extend to all types, not just object types.

Most cases are standard, and follow conventional programming language theory (for structural subtyping, specifically).

Other notable cases in Motoko for new programmers include array, options, variants and number type inter-relationships.