Mutable states
Each actor in Motoko may use, but may never directly share, internal mutable state.
Later, we discuss sharing among actors, where actors send and receive immutable data, and also handles to each others external entry points, which serve as shareable functions. Unlike those cases of shareable data, a key Motoko design invariant is that mutable data is kept internal (private) to the actor that allocates it, and is never shared remotely.
In this chapter, we continue using minimal examples to show how to introduce (private) actor state, and use mutation operations to change it over time.
In local objects and classes, we introduce the
syntax for local objects, and a minimal counter
actor with a single
mutable variable. In the following chapter, we
show an actor with the same behavior, exposing the counter variable
indirectly behind an associated service interface for using it
remotely.
Immutable versus mutable variables
The var
syntax declares mutable variables in a declaration
block:
let text : Text = "abc";
let num : Nat = 30;
var pair : (Text, Nat) = (text, num);
var text2 : Text = text;
The declaration list above declares four variables.
The first two variables (text
and num
) are lexically-scoped, immutable variables.
The final two variables (pair
and text2
) are lexically-scoped, mutable variables.
Assignment to mutable memory
Mutable variables permit assignment, and immutable variables do not.
If we try to assign new values to either text
or num
above, we
will get static type errors; these variables are immutable.
However, we may freely update the value of mutable variables pair
and text2
using the syntax for assignment, written as :=
, as follows:
text2 := text2 # "xyz";
pair := (text2, pair.1);
pair
Above, we update each variable based on applying a simple “update
rule” to their current values (for example, we update text2
by
appending string constant "xyz"
to its suffix). Likewise, an actor
processes some calls by performing updates on its internal (private)
mutable variables, using the same assignment syntax as above.
Special assignment operations
The assignment operation :=
is general, and works for all types.
Motoko also includes special assignment operations that combine assignment with a binary operation. The assigned value uses the binary operation on a given operand and the current contents of the assigned variable.
For example, numbers permit a combination of assignment and addition:
var num2 = 2;
num2 += 40;
num2
After the second line, the variable num2
holds 42
, as one would expect.
Motoko includes other combinations as well. For example, we can rewrite the line above that updates text2
more concisely as:
text2 #= "xyz";
text2
As with +=
, this combined form avoids repeating the assigned
variable’s name on the right hand side of the (special) assignment
operator #=
.
The full list of assignment operations lists numerical, logical, and textual operations over appropriate types (number, boolean and text values, respectively).
Reading from mutable memory
When we updated each variable, we also first read from the mutable contents, with no special syntax.
This illustrates a subtle point: Each use of a mutable variable looks like the use of an immutable variable, but does not act like one. In fact, its meaning is more complex. As in many languages (JavaScript, Java, C#, etc.), but not all, the syntax of each use hides the memory effect that accesses the memory cell identified by that variable, and gets its current value. Other languages from functional traditions (SML, OCaml, Haskell, etc), generally expose these effects syntactically.
Below, we explore this point in detail.
Understanding var
- versus let
-bound variables
Consider the following two variable declarations, which look similar:
let x : Nat = 0
and:
var x : Nat = 0
The only difference in their syntax is the use of keyword let
versus
var
to define the variable x
, which in each case the program
initializes to 0
.
However, these programs carry different meanings, and in the context of larger programs, the difference in meanings will impact the meaning of each occurrence of x
.
For the first program, which uses let
, each such occurrence means 0
. Replacing each occurrence with 0
will not change the meaning of the program.
For the second program, which uses var
, each occurrence means: “read and produce the current value of the mutable memory cell named x
.”
In this case, each occurrence’s value is determined by dynamic state: the contents of the mutable memory cell named x
.
As one can see from the definitions above, there is a fundamental contrast between the meanings of let
-bound and var
-bound variables.
In large programs, both kinds of variables can be useful, and neither kind serves as a good replacement for the other.
However, let
-bound variables are more fundamental.
To see why, consider encoding a var
-bound variable using a one-element, mutable array, itself bound using a let
-bound variable.
For instance, instead of declaring x
as a mutable variable initially holding 0
, we could instead use y
, an immutable variable that denotes a mutable array with one entry, holding 0
:
var x : Nat = 0 ;
let y : [var Nat] = [var 0] ;
We explain mutable arrays in more detail below.
Unfortunately, the read and write syntax required for this encoding
reuses that of mutable arrays, which is not as readable as that of
var
-bound variables. As such, the reads and writes of variable x
will be easier to read than those of variable y
.
For this practical reason, and others, var
-bound variables are a
core aspect of the language design.
Immutable arrays
Before discussing mutable arrays, we introduce immutable arrays, which share the same projection syntax, but do not permit mutable updates (assignments) after allocation.
Allocate an immutable array of constants
let a : [Nat] = [1, 2, 3] ;
The array a
above holds three natural numbers, and has type [Nat]
.
In general, the type of an immutable array is [_]
, using square
brackets around the type of the array’s elements, which must share a
single common type, in this case Nat
.
Project from (read from) an array index
We can project from (read from) an array using the usual bracket
syntax ([
and ]
) around the index we want to access:
let x : Nat = a[2] + a[0] ;
Every array access in Motoko is safe. Accesses that are out of bounds will not access memory unsafely, but instead will cause the program to trap, as with an assertion failure.
The Array module
The Motoko standard library provides basic operations for immutable and mutable arrays. It can be imported as follows,
import Array "mo:base/Array";
In this section, we discuss some of the most frequently used array operations. For more information about using arrays, see the Array library descriptions.
Allocate an immutable array with varying content
Above, we showed a limited way of creating immutable arrays.
In general, each new array allocated by a program will contain a varying number of varying elements. Without mutation, we need a way to specify this family of elements "all at once", in the argument to allocation.
To accommodate this need, the Motoko language provides the
higher-order array-allocation function Array.tabulate
, which
allocates a new array by consulting a user-provided "generation
function" gen
for each element.
func tabulate<T>(size : Nat, gen : Nat -> T) : [T]
Function gen
specifies the array as a function value of arrow
type Nat → T
, where T
is the final array element type.
The function gen
actually functions as the array during
its initialization: It receives the index of the array element, and it produces the element (of type T
) that should reside at that index in the array.
The allocated output array populates itself based on this specification.
For instance, we can first allocate array1
consisting of some initial constants, and then functionally-update some of the indices by "changing" them (in a pure, functional way), to produce array2
, a second array that does not destroy the first.
let array1 : [Nat] = [1, 2, 3, 4, 6, 7, 8] ;
let array2 : [Nat] = Array.tabulate<Nat>(7, func(i:Nat) : Nat {
if ( i == 2 or i == 5 ) { array1[i] * i } // change 3rd and 6th entries
else { array1[i] } // no change to other entries
}) ;
Even though we "changed" array1
into array2
in a functional sense, notice that both arrays and both variables are immutable.
Next, we consider mutable arrays, which are fundamentally distinct.
Mutable arrays
Above, we introduced immutable arrays, which share the same projection syntax as mutable arrays, but do not permit mutable updates (assignments) after allocation. Unlike immutable arrays, each mutable array in Motoko introduces (private) mutable actor state.
Because Motoko’s type system enforces that remote actors do not share their mutable state, the Motoko type system introduces a firm distinction between mutable and immutable arrays that impacts typing, subtyping and the language abstractions for asynchronous communication.
Locally, the mutable arrays can not be used in places that expect immutable ones, since Motoko’s definition of subtyping for arrays (correctly) distinguishes those cases for the purposes of type soundness. Additionally, in terms of actor communication, immutable arrays are safe to send and share, while mutable arrays can not be shared or otherwise sent in messages. Unlike immutable arrays, mutable arrays have non-shareable types.
Allocate a mutable array of constants
To indicate allocation of mutable arrays (in contrast to the forms above, for immutable ones), the mutable array syntax [var _]
uses the var
keyword, in both the expression and type forms:
let a : [var Nat] = [var 1, 2, 3] ;
As above, the array a
above holds three natural numbers, but has type [var Nat]
.
Allocate a mutable array with dynamic size
To allocate mutable arrays of non-constant size, use the Array_init
primitive, and supply an initial value:
func init<T>(size : Nat, x : T) : [var T]
For example:
var size : Nat = 42 ;
let x : [var Nat] = Array.init<Nat>(size, 3);
The variable size
need not be constant here; the array will have size
number of entries, each holding the initial value 3
.
Mutable updates
Mutable arrays, each with type form [var _]
, permit mutable updates via assignment to an individual element, in this case element index 2
gets updated from holding 3
to instead hold value 42
:
let a : [var Nat] = [var 1, 2, 3];
a[2] := 42;
a
Subtyping does not permit mutable to be used as immutable
Subtyping in Motoko does not permit us to use a mutable array of type [var Nat]
in places that expect an immutable one of type [Nat]
.
There are two reasons for this. First, as with all mutable state, mutable arrays require different rules for sound subtyping. In particular, mutable arrays have a less flexible subtyping definition, necessarily. Second, Motoko forbids uses of mutable arrays across asynchronous communication, where mutable state is never shared.