Local objects and classes

In addition to (remote) actor objects, Motoko provides local objects that are similar in their syntax, typing and evaluation to ordinary (local) objects from object-oriented programming.

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, a la 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.

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 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 byte_counter object

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

object byte_counter = {
  var count : Nat8 = 0;
  public func inc() { count += 1 };
  public func read() : Nat { nat8ToNat(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 bump_counter = {
  var c = 0; public func bump() : Nat { c += 1; c };
};

The object bump_counter 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:

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

Here, we consider a counter named full_counter 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.

As aside for language theorists and advanced readers: 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.

Subtyping in general. 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.

Object classes

to do: examples and prose here