命令的な制御フロー
制御フローには2つの重要なカテゴリーがあります。
-
if
やswitch
式のように、ある値の構造が、制御や次に評価する式の選択を促すとき、宣言的 制御フローと呼ばれます。 -
プログラマの命令に応じて制御が突然変更され、通常の制御フローを放棄するような場合、命令的 制御フローと呼ばれます。例としては、
break
やcontinue
、return
やthrow
があります。
命令的な制御フローは、ステート変化やエラー処理や入出力などの副作用と密接に関係していることが多いです。
関数からの早期 return
通常、関数から返す値は関数本体の全体を評価した結果の値です。しかしながら、時には関数本体の評価の終了前に結果が得られることがあります。このような状況では、return ⟨式⟩
構文を使用し、残りの計算を放棄して結果を持って直ちに関数を終了することができます。
throw
の使用が許可されている場合には、関数本体で throw
を使用することで、エラーが発生したときに計算を放棄することができます。
関数の結果がユニット型の場合、return ()
の代わりに、短縮形の return
を使用することができます。
ループとラベル
Motoko では、以下のような数種類の繰り返し構造が用意されています。
-
構造化されたデータのメンバを反復処理するための
for
式。 -
プログラムによる繰り返しのための
loop
式(オプションで終了条件を与える)。 -
プログラムによる繰り返しのための
while
ループ(入口に条件式を与える)。
これらはいずれも、ループにシンボリックな名前を付けるために、label ⟨ラベル名⟩
という修飾子を前に付けることができます。名前付きループは、ループの入口に戻って処理を継続したり(continue)、ループ処理を中断(break)するように命令的に制御フローを変更するのに便利です。
-
continue ⟨ラベル名⟩
を使ってループの入口に戻るか、 -
break ⟨ラベル名⟩
を使ってループから抜ける
以下の例では、for
式はあるテキストの文字列に対してループし、文字が感嘆符の場合にすぐにイタレーションを破棄します。
import Debug "mo:base/Debug";
label letters for (c in "ran!!dom".chars()) {
Debug.print(debug_show(c));
if (c == '!') { break letters };
// ...
}
ラベル付けされた式
label
には他にも 2 つの側面があり、あまり主流ではありませんが、ある特定の状況では便利です。
-
label
は型付け可能です。 -
(ループに限らず)いかなる 式にも label を前に付けることで名前を付けることができます。
break
を指定すると、式の結果に即座に値を与えて、式の評価を短絡させることができます。(これはreturn
を使って関数を早期に終了させるのと似ていますが、関数を宣言して呼び出すというオーバーヘッドがありません。)
型注釈されたラベルの構文は、label ⟨ラベル名⟩ : ⟨型⟩ ⟨式⟩
となります。任意の式を break ⟨ラベル名⟩ ⟨別の式⟩
構造を使って終了し、 ⟨別の式⟩
の値を返して ⟨式⟩
の評価を短絡させることができます。
これらの構造をうまく使うことで、プログラマは主要なプログラムロジックに集中しつつ、break
を使って例外的なケースを処理することができます。
import Text "mo:base/Text";
import Iter "mo:base/Iter";
type Host = Text;
let formInput = "us@dfn";
let address = label exit : ?(Text, Host) {
let splitted = Text.split(formInput, #char '@');
let array = Iter.toArray<Text>(splitted);
if (array.size() != 2) { break exit(null) };
let account = array[0];
let host = array[1];
// if (not (parseHost(host))) { break exit(null) };
?(account, host)
}
当然ながら、ラベル付けされた普通の(ループではない)式では continue
は使えません。型付けに関しては、⟨式⟩
と ⟨別の式⟩
の両方の型がラベルが宣言した ⟨型⟩
と一致する必要があります。ラベルに ⟨ラベル名⟩
だけが与えられている場合、そのデフォルトの ⟨型⟩
はユニット型 (()
) になっています。同様に、⟨別の式⟩
のない break
は unit (()
) という値の略記となります。
Option ブロックと null 値ブレーク
他の多くの高級言語と同様に、Motoko では null
値を使用することができ、?T
形式の Option 型を使って null
値が発生する可能性を追跡できます。
これは、可能な限り null
値を使用しないようにすることと、必要なときに null
値である可能性を考慮することの両方を目的としています。
もし null
値をテストする唯一の方法が冗長な switch
式なら後者は面倒だったかもしれませんが、Motoko では Option ブロック や null 値ブレーク といった専用の構文で Option 型の取り扱いを簡単化しています。
Option ブロック do ? <ブロック>
は、<ブロック>
が T
型であるときに ?T
型の値を生成します。<ブロック>
からブレークされる可能性があることが重要です。
<ブロック>
の中で、null ブレーク <式> !
は、無関係な Option 型 ?U
の <式>
の結果が null
であるかどうかをテストします。
もし <式>
の結果が null
ならば、制御は直ちに do ? <ブロック>
を終了し、その値は null
となります。
結果が null
でなければ、<式>
の結果は Option 値 ?v
でなければならず、<式> !
の評価はその内容である v
(U
型) で進められます。
実際の例として、自然数、除算、ゼロかどうかのテストからなる数値式 Exp
を評価する簡単な関数 eval
を定義し、バリアント型としてエンコードしたものを以下に示します。
type Exp = {
#Lit : Nat;
#Div : (Exp, Exp);
#IfZero : (Exp, Exp, Exp);
};
func eval(e : Exp) : ? Nat {
do ? {
switch e {
case (#Lit n) { n };
case (#Div (e1, e2)) {
let v1 = eval e1 !;
let v2 = eval e2 !;
if (v2 == 0)
null !
else v1 / v2
};
case (#IfZero (e1, e2, e3)) {
if (eval e1 ! == 0)
eval e2 !
else
eval e3 !
};
};
};
}
0
による除算をトラップせずに防ぐために、eval
関数は失敗を示す null
を使用して Option 型の結果を返します。
それぞれの再帰呼び出しは !
を使って null
かどうかをチェックし、結果が null
の場合は直ちに外側の do ? block
と関数そのものを null
値を持って終了します。
( Option ブロックの簡潔さを理解する演習として、ラベル付きの式を使用して eval
を書き換えて、null 値ブレークごとに明示的に switch 式を入れてみるとよいでしょう。)
loop
を使った反復処理
一連の命令式を無限に繰り返す最も簡単な方法は、loop
構造を使うことです。
loop { ⟨expr1⟩; ⟨expr2⟩; ... }
ループを抜けるには、return
または break
構造を使用します。
ループを条件付きで繰り返すために、loop ⟨本体⟩ while ⟨条件⟩
という再投入条件を付けることができます。
このようなループの本体は、常に少なくとも一度は実行されます。
前提条件付きの while
ループ
ループの最初の実行を防ぐために、入力条件が必要な場合があります。このようなループ処理のために、 while ⟨条件⟩ ⟨本体⟩
式が利用可能です。
while (earned < need) { earned += earn() };
loop
とは異なり、while
ループの本体は一度も実行されない可能性があります。
イテレーションのための for
ループ
ある同種のコレクションの要素に対する反復は、for
ループを使って行うことができます。値はイテレータから引き出され、順番にループパターンに束縛されます。
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;
};
inventory
for
ループにおける range
の使用
range
関数は、与えられた下限値と上限値を持つ(Iter<Nat>
型の)イテレータを生成します。
次のループの例では、11 回の反復処理で 0
から 10
までの数字を表示します。
import Iter "mo:base/Iter";
import Debug "mo:base/Debug";
var i = 0;
for (j in Iter.range(0, 10)) {
Debug.print(debug_show(j));
assert(j == i);
i += 1;
};
assert(i == 11);
より一般的には、range
関数は自然数列に対するイテレータを生成する class
です。それぞれのイテレータは Iter<Nat>
型となります。
コンストラクタ関数として、range
は以下の関数型を持ちます。
(lower : Nat, upper : Int) -> Iter<Nat>
ここで Iter<Nat>
は next
メソッドを持つイテレータオブジェクト型であり、それぞれ ?Nat
型の Option 要素を生成します。
type Iter<A> = {next : () -> ?A};
各呼び出しに対して、next
は(?Nat
型の)Option 要素を返します。
null
値は、反復処理のシーケンスが終了したことを示します。
null
に達するまで、ある数 n の ?
nの形式の非 null
値には、繰り返し処理における次の順番の要素が含まれます。
revRange
を使う
range
と同様に、revRange
関数は(それぞれ Iter<Int>
型の)イテレータを生成する class
です。
コンストラクタ関数として、この関数は以下の関数型を持っています。
(upper : Int, lower : Int) -> Iter<Int>
range
とは異なり、revRange
関数は、最初の 上限 値から最後の 下限 値まで、繰り返し計算の順序を 降順 にします。
特定のデータ構造のイテレータを使用する
多くの組み込みデータ構造には、あらかじめイテレータが定義されています。以下の表は、それらの一覧です。
型 | 名前 | イテレータ | 要素 | 要素の型 |
---|---|---|---|---|
|
|
|
配列の要素 |
|
|
|
|
配列の有効なインデックス |
|
|
|
|
配列の要素 |
|
|
|
|
配列の有効なインデックス |
|
|
テキスト |
|
テキストの文字列 |
|
|
Blob |
|
Blob のバイト |
|
ユーザー定義のデータ構造では、独自のイテレータを定義することができます。ある要素型 A
に対して Iter<A>
型になっている限り、これらは組み込みのものと同じように振る舞い、通常の for
ループで使用することができます。