Imperative control flow

There are two key categories of control flow:

  • declarative, when the structure of some value guides control and the selection of the next expression to evaluate, like in if and switch expressions;

  • imperative where control changes abruptly according to a programmer’s command, abondoning regular control flow; examples are break and continue, but also return and throw.

Imperative control flow often goes hand-in-hand with state changes and other flavors of side-effects, such as error handling and input/output.

Early return from func

Normally, the result of a function is the value of its body. Sometimes, during evaluation of the body, the result is available before the end of evaluation. In such situations the return ⟨exp⟩ construct can be used to abandon the rest of the computation and immediately exit the function with a result. Similarly, where permitted, throw may be used to abandon a computation with an error.

When a function has unit result type, the shorthand return may be used instead of the equivalent return ().

Loops and labels

Motoko provides several kinds of repetition constructs, including:

  • for expressions for iterating over members of structured data.

  • loop expressions for programmatic repetition (optionally with termination condition).

  • while loops for programmatic repetition with entry condition.

Any of these can be prefixed with a label ⟨name⟩ qualifier to give the loop a symbolic name. Named loops are useful for imperatively changing control flow to continue from the entry or exit of the named loop.

  • re-entering the loop with continue ⟨name⟩, or

  • exiting the loop altogether with break ⟨name⟩.

In the following example, the for expression loops over characters of some text and abandons iteration as soon as an exclamation sign is encountered.

label letters for (c in someText.chars()) {
  if (c == '!') { break letters };
  ...
}

Labeled expressions

There are two other facets to label​s that are less mainstream, but come in handy in certain situations:

  • label​s can be typed

  • any expression (not just loops) can be named by prefixing it with a label; break allows one to short-circuit the expression’s evaluation by providing an immediate value for its result. (This is similar to exiting a function early using return, but without the overhead of declaring and calling a function.)

The syntax for type-annotated labels is label ⟨name⟩ : ⟨type⟩ ⟨expr⟩, signifying that any expression can be exited using a break ⟨name⟩ ⟨alt-expr⟩ construct that returns the value of <alt-expr> as the value of ⟨expr⟩, short-circuiting evaluation of <expr>.

Judicious use of these constructs allows the programmer to focus on the primary program logic and handle exceptional case via break

let address = label exit : ?(Text, IPAddr) {
  let splitted = split('@', formInput);
  if splitted == null { break exit(null) };
  let ?(account, host) = splitted;
  if (not (parseAccount(account))) { break exit(null) };
  ...
}

Naturally, labeled common expressions don’t allow continue. In terms of typing, both ⟨expr⟩ and ⟨alt-expr⟩​'s types must conform with the label’s declared ⟨type⟩. If a label is only given a ⟨name⟩, then its ⟨type⟩ defaults to unit (()). Similarly a break without an ⟨alt-expr⟩ is shorthand for the value unit (()).

Repetition with loop

The simplest way to indefinitely repeat a sequence of imperative expressions is by using a loop construct

loop { ⟨expr1⟩; ⟨expr2⟩; ... }

The loop can only be abandoned with a return or break construct.

A re-entry condition can be affixed to allow a conditional repetition of the loop with loop ⟨body⟩ while ⟨cond⟩.

The body of such a loop is always executed at least once.

while loops with precondition

Sometimes an entry condition is needed to guard the first execution of a loop. For this kind of repetition the while ⟨cond⟩ ⟨body⟩-flavor is available

while (earned < need) { earned += earn() };

Unlike a loop, the body of a while loop may never be executed.

for loops for iteration

An iteration over elements of some homogeneous collection can be performed using a for loop. The values are drawn from an iterator and bound to the loop pattern in turn.

let carsInStock = [
  ("Buick", 2020, 23.000),
  ("Toyota", 2019, 17.500),
  ("Audi", 2020, 34.900)
];
var inventory : { var value : Float } = { var value = 0.0 };
for ((model, year, price) in carsInStock.vals()) {
  inventory.value += price;
}

Using range with a for loop

The range function produces an iterator (of type Iter<Nat>) with the given lower and upper bound, inclusive.

The following loop example prints the numbers 0 through 10 over its eleven iterations:

var i = 0;
for (j in range(0, 10)) {
 printNat(j);
 assert(j == i);
 i += 1;
};
assert(i == 11);

More generally, the function range is a class that constructs iterators over sequences of natural numbers. Each such iterator has type Iter<Nat>.

As a constructor function, range has a function type:

(lower:Nat, upper:Nat) -> Iter<Nat>

Where Iter<Nat> is an iterator object type with a next method that produces optional elements, each of type ?Nat:

type Iter<A> = {next : () -> ?A};

For each invocation, next returns an optional element (of type ?Nat).

The value null indicates that the iteration sequence has terminated.

Until reaching null, each non-null value, of the form ?n for some number n, contains the next successive element in the iteration sequence.

Using revrange

Like range, the function revrange is a class that constructs iterators (each of type Iter<Nat>). As a constructor function, it has a function type:

(upper:Nat, lower:Nat) -> Iter<Nat>

Unlike range, the revrange function descends in its iteration sequence, from an initial upper bound to a final lower bound.

Using iterators of specific data structures

Many built-in data structures come with pre-defined iterators. Below table lists them

Table 1. Iterators for data structures
Type Name Iterator Elements Element type

[T]

array of T​s

vals

the array’s members

T

[T]

array of T​s

keys

the array’s valid indices

Nat

[var T]

mutable array of T​s

vals

the array’s members

T

[var T]

mutable array of T​s

keys

the array’s valid indices

Nat

Text

text

chars

the text’s characters

Char

Blob

blob

bytes

the blob’s bytes

Word8

User-defined data structures can define their own iterators. As long they conform with the Iter<A> type for some element type A, these behave like the built-in ones and can be consumed with ordinary for-loops.