基本的なコンセプトと用語
Motoko は、Actor を使った分散型プログラミングのために設計されています。
Internet Computer 上で Motoko を使ってプログラミングする場合、各 Actor は Motoko、Rust、Wasm、または Wasm にコンパイルされた他の言語など、その記述言語に関わらず、Candid インターフェースを持つ Internet Computer Canister スマートコントラクト を表します。Motoko 内では、Internet Computer にデプロイされる任意の言語で書かれた Canister を指すために Actor という用語を使用します。Motoko の役割は、これらの Actor を簡単に作成し、デプロイ後にプログラムで簡単に使用できるようにすることです。
Actor を使って分散型アプリケーションを書き始める前に、プログラミング言語の基本的な構成要素、特に Motoko について知っておく必要があります。 この章では、以降のドキュメントで使用されている、Motoko を使ったプログラミングを学ぶ上で欠かせない以下のような重要な概念や用語を紹介します:
プログラム
宣言
式
値
変数
型
他の言語でのプログラミング経験がある方や、モダンプログラミング言語の理論に精通している方は、これらの用語やその使われ方に既に慣れていることでしょう。 これらの用語の使われ方は、Motoko でも特に変わった点はありません。 しかし、プログラミングに慣れていない方のために、このガイドでは、Actor や分散型プログラミングの使用を避けたシンプルなサンプルプログラムを用いて、これらの用語を徐々に紹介していきます。 基本的な用語を理解した後、言語のより高度な部分を学ぶことができます。 より高度な機能は、その複雑さに伴い、より複雑な例で説明されています。
この章では、以下のトピックについて説明します:
Motoko プログラムの構文
各 Motoko プログラム は、宣言と式を自由に組み合わせたもので、それらの構文的な分類は異なりますが、互いに関連しています(プログラムの正確な構文については、言語のクイックリファレンスガイドを参照して下さい)。
私たちが Internet Computer 上にデプロイするプログラムでは、有効なプログラムは Actor 式で構成されており、Actor と async データで説明するように、特定の構文(actor
キーワード)が用いられます。
議論の準備として、本章と ミュータブルなステート の中で、Internet Computer の Service ではなく、Service を書くための Motoko のコードスニペットを用いて説明しています。それぞれは通常、(Service ではない) Motoko プログラムとして単独で実行することができ、場合によってはターミナルの出力をプリントすることもできます。
この章の例では、四則演算などの簡単な式を使って、Motoko の基本原理を説明します。 Motoko の全ての式の構文について知りたい方は、言語のクイックリファレンスを参照してください。
まず初めに、次のコードスニペットは、変数 x
と y
の 2 つの宣言で構成されており、続く式で 1 つのプログラムを形成しています:
let x = 1;
let y = x + 1;
x * y + x;
以下の議論では、この小さなプログラムの変形を使用します。
まず、このプログラムの型は Nat
(自然数)であり、実行すると 3
という自然数の値に評価されます。
中括弧で囲まれたブロック(do {
と }
)と別の変数(z
)を導入することで、元のプログラムを次のように修正することができます:
let z = do {
let x = 1;
let y = x + 1;
x * y + x
};
宣言と式
宣言は、イミュータブル(不変)な変数、ミュータブル(可変)なステート、Actor、オブジェクト、クラス、その他の型を導入します。 式は、これらを使った計算を記述します。
ここでは、イミュータブルな変数を宣言し、簡単な演算を行うプログラムを例に説明します。
宣言と式の違い
各 Motoko プログラム は、宣言と式を自由に組み合わせたもので、それらの構文的な分類は異なるものの互いに関連しているということを思い出しましょう。 この章では、例を使って宣言と式の区別を説明し、それらを混在させて使うことに慣れていきます。
上で最初に紹介したサンプルプログラムを思い出しましょう:
let x = 1;
let y = x + 1;
x * y + x;
実際のところ、このプログラムは以下の 3 個の宣言からなる 宣言のリスト です:
宣言
let x = 1;
によるイミュータブルな変数x
と、宣言
let y = x + 1;
によるイミュータブルな変数y
と、最終的な式の値である
x * y + x
を保持する 匿名の 暗黙的変数。
この式 x * y + x
は、より一般的な原理を示しています。 それぞれの式は、その式の結果の値で無名変数を暗黙的に宣言しているので、必要に応じて宣言と考えることができます。
式が最後の宣言として現れる場合、式は任意の型を持つことができます。ここでは、式 x * y + x
の型は Nat
です。
式が宣言リストの最後ではなく、宣言リストの中にある場合は、式はユニット型 ()
でなければなりません。
宣言リストにおけるユニット型でない式の無視
このユニット型でなければならないという制限は、ignore
を明示的に使用して、使用されていない結果の値を無視することで切り抜けることができます。 例えば、以下のようになります:
let x = 1;
ignore(x + 42);
let y = x + 1;
ignore(y * 42);
x * y + x;
宣言と変数の代入
宣言は相互再帰可能ですが、そうでない書き方のために代入セマンティクスを使うことができます(高校数学における式の単純化でおなじみの、等式を別の等式に代入すること。)
元の例を思い出してみましょう:
let x = 1;
let y = x + 1;
x * y + x;
変数の宣言値をそれぞれの出現箇所に 代入 することで、上のプログラムを手動で書き換えることができます。 そうすることで次のような式になり、これも有効なプログラムです。
1 * (1 + 1) + 1
上の式も、元のプログラムと同じ型で、同じ動作(結果値 3
)をする有効なプログラムです。 また、ブロックを使って一つの式を形成することもできます。
宣言からブロック式への変換
上記のプログラムの多くは、先ほどの例のように宣言のリストで構成されています:
let x = 1;
let y = x + 1;
x * y + x
宣言リストそれ自体は 式 ではないので、その最終値(3
)を使って別の変数を(すぐに)宣言することはできません。
ブロック式:この宣言リストを 中括弧 で囲むことで、ブロック式 を形成することができます。ブロックは、if
、loop
、case
などの制御フローのサブ式としてのみ使用できます。それ以外の場所では、do { … }
を使ってブロック式を表現し、ブロックをオブジェクトリテラルと区別しています。例えば、do {}
は ()
型の空のブロックで、{}
は {}
型の空のレコードです。
do {
let x = 1;
let y = x + 1;
x * y + x
}
これも有効なプログラムですが、宣言された変数 x
と y
が、導入したブロック内のプライベートスコープになっています。
このブロック形式は、宣言リストとその 変数名の選択 の自律性を維持するのに役立ちます。
ブロック式は値を生成し、括弧で囲むとより大きな複合式の中に出現させることができます。
100 +
(do {
let x = 1;
let y = x + 1;
x * y + x
})
宣言は 字句スコープ に従う
上の例では、ブロックを入れ子にすることで、それぞれの宣言リストとその変数名の選択の自律性が保たれることを説明しました。 言語理論家はこの考え方を 字句スコープ と呼んでいます。 つまり、変数のスコープはネストしても構わないが、ネストする際に干渉してはいけないということです。
例えば、次の(ブロックの外の)プログラムは、2 ではなく 42 と評価されます。なぜなら、最後の行で出現する x
と y
は、囲まれたブロック内の定義ではなく、最初の行の 定義を参照しているからです。
let x = 40; let y = 2;
ignore do {
let x = 1;
let y = x + 1;
x * y + x
};
x + y
字句スコープを持たない他の言語では、このプログラムは異なる結果となるかもしれません。 しかし、モダンな言語では普遍的に字句スコープが好まれています。
数学的な明快さはさておき、字句スコープの実用的な利点は 安全性 であり、組成的に安全なシステムを構築する際に使用されます。 具体的には、Motoko は非常に強力な構成上の特性を持っています。例えば、信頼していないプログラムの中に自分のプログラムを入れ子にしても、入れ子の外のプログラムが、あなたの変数を恣意的に異なる意味に再定義することはできません。
値と評価
ひとたび Motoko の式がプログラムの制御の(シングル)スレッドを受け取ると、結果の値 になるまで張り切って評価します。
その際、一般的には 環境側の制御スタック からの制御を放棄する前に、サブ式やサブルーチンに制御を渡します。
もしこの式が値の形式に到達しない場合、式は無限に評価され続けます。 後ほど再帰関数や命令型制御フローを紹介しますが、これらはいずれも終了しない処理を許容するものです。 ここでは、結果として値が得られる、終了するプログラムのみを考えます。
上の例では、自然数を生成する式に焦点を当てて説明しました。 言語のより広範な概要として、以下に他の値の形式について簡単にまとめます:
プリミティブな値
Motoko では、以下のプリミティブな値の形式を利用することができます:
ブール値 (
true
とfalse
).整数 (…,
-2
,-1
,0
,1
,2
, …) - 制限付き整数と 制限なし 整数自然数 (
0
,1
,2
, …) - 制限付き自然数と 制限なし 自然数テキスト値 - ユニコード文字の文字列
デフォルトでは、整数 と 自然数 は 制限なし で、オーバーフローしません。 その代わり、いかなる有限の数にも対応できるように、伸長する表現を使用しています。
実用性の観点から、Motoko には、デフォルトの制限なし数値とは別に、整数や自然数の 制限付き の型も含まれています。 それぞれの制限付き数値の型は、固定長(8
, 16
, 32
, 64
のいずれか)を持ち、それぞれに “オーバーフロー” の可能性があります。このイベントが発生するとエラーとなり、プログラムはトラップします。
Motoko では、明示的に ラッピング 操作(演算子の中の %
文字で示されます)を行う場合などの十分に定義された状況を除いて、チェック・キャッチできないオーバーフローはありません。 Motoko では、さまざまな数値表現を変換するためのプリミティブな組み込み関数が用意されています。
言語のクイックリファレンスは、プリミティブ型の完全なリストを提供しています。
非プリミティブな値
Motoko では、上記のプリミティブな値と型に加えて、ユーザー定義の型や、以下の非プリミティブな値の形式とそれに関連する型を利用することができます。
タプル(ユニット値である "空のタプル" を含む)
配列(イミュータブルな配列とミュータブルな配列の両方)
オブジェクト(匿名の順序なしフィールドとメソッドを含む)
バリアント(名前付きコンストラクタとオプショナルのペイロード値を含む)
Async 値(promise や future としても知られる)
エラー値(例外やシステム障害のペイロードを運ぶ)
これらの形式の使用については後の章で説明します。 プリミティブな値と非プリミティブな値の正確な言語定義については、言語のクイックリファレンスを参照してください。
unit 型 vs void型
Motoko には void
という名前の型はありません。 Java や C++ などの言語を使っていて、返り値の型が “void” だと思っている読者も多いと思いますが、代わりに ()
と書かれた ユニット型 を思い浮かべるようにしてみてください。
実用的には、void
と同様に、ユニット値は一般的に表現上のコストがありません。
void
型とは異なり、ユニット値は 存在します が、void
型の返り値と同様、ユニット値は内部的には何の値も持たず、情報 は常にゼロです。
ユニット値を数学的に考えるもう一つの方法は、要素を持たないタプル("nullary" ならぬ “zero-ary”)です。このようなプロパティを持つ値は 1 つしかないので、数学的に一意であり、よって実行時に表現する必要はありません。
自然数
この型のメンバは、通常の値である 0
, 1
, 2
, … で構成されていますが、数学のように Nat
が特別な最大サイズに制限されることはありません。 これらの値のランタイム表現は任意の大きさの数値に対応しており、"オーバーフロー" を(ほぼ)不可能にしています。 (ほぼ 不可能というのは、プログラムのメモリが足りなくなるのと同じことで、極端な状況下ではプログラムによっては常に起こりうることだからです。)
Motoko では、通常の算術演算が可能です。 例として、次のようなプログラムを考えてみましょう:
let x = 42 + (1 * 37) / 12: Nat
このプログラムは Nat
型の値 45
と評価されます。
型の健全性
各 Motoko の式に対して型チェックが行われることを、正しく型付けされている と呼んでいます。Motoko の式の 型 は、プログラムが実行されたときの将来の動作についての、言語から開発者への約束事の役割を果たします。
まず、正しく型付けされたプログラムは、未定義の動作をすることなく評価されます。 これには、正しく型付けされたプログラムは間違いを起こさない
という言葉が当てはまります。 この言葉の深い意味を知らない人のために補足すると、意味のある(曖昧さのない)プログラムには厳密な(概念としての)空間があり、型システムはその空間の中に留まることを強制しているため、すべての正しく型付けされたプログラムは、正確な(曖昧さのない)意味を持っているということです。
さらに言えば、型はプログラムの結果を正確に予測します。 制御を得れば、プログラムは元のプログラムの結果の型と一致する 結果の値 を生成します。
いずれにしても、プログラムの静的な見方と動的な見方は、静的な型システムによってリンクされ、互いに一致します。 この静的な見方と動的な見方の一致は静的な型システムの中心的な原理であり、設計の中核的な側面として Motoko によって提供されています。
この型システムは、非同期のインタラクションがプログラムの静的・動的な見方が一致していること、そして "ボンネットの下で(内部で)" 生成された結果のメッセージが実行時に不一致にならないことも強制します。 この一致は、型付けされた言語で通常期待される、呼び出し元と呼び出された側の引数の型や返り値の型が一致することと、その精神が似ています。
型アノテーションと変数
変数は、(静的な)名前や(静的な)型と、実行時にのみ存在する(動的な)値を関連付けます。
この意味で、Motoko の型は、プログラムのソースコードの中で、コンパイラが検証した信頼できるドキュメント を提供します。
以下の非常に短いプログラムを考えてみましょう:
let x : Nat = 1
この例では、コンパイラは式 1
が Nat
型であり、x
も同じ型であることを推定します。
この場合、プログラムの意味を変えることなく、この型アノテーションを省略することができます:
let x = 1
演算子のオーバーロードを伴うような難解な状況を除いて、型アノテーションは(通常は)実行中のプログラムの意味に影響を与えません。
型アノテーションが省略された状態でコンパイルが通った場合、上記のケースのように、プログラムは元のプログラムと同じ意味(同じ 動作 )となります。
しかし、コンパイラが他の前提条件を推測したり、プログラム全体をチェックしたりするために、型アノテーションが必要になることがあります。
型アノテーションを追加してもコンパイルが通る場合、追加された型アノテーションは既存の型アノテーションと 矛盾がない ことがわかります。
例えば、(必須ではない)型アノテーションを追加することで、コンパイラはすべての型アノテーションと他の推論された事実が全体として一致していることをチェックします。
let x : Nat = 1 : Nat
しかし、アノテーションの型と 矛盾すること をしようとすると、タイプチェッカーはエラーを知らせます。
以下のような、正しく型付けされていないプログラムを考えましょう:
let x : Text = 1 + 1
1 + 1
の型は Nat
であって Text
ではなく、また Nat
と Text
はサブタイプの関係にないので、型アノテーションの Text
はその後のプログラムと一致しません。 結果的に、このプログラムは正しく型付けされておらず、コンパイラはエラーメッセージとエラー箇所を知らせ、コンパイルも実行もしません。
型のエラーとメッセージ
数学的には、Motoko の型システムは 宣言的 であり、形式論理の概念として実装とは無関係に存在しています。 同様に、言語定義の他の重要な側面(例:実行セマンティクス)も、実装の外に存在しています。
しかし、この論理定義を設計し、試し、間違いを犯す練習をするために、私たちはこの型システムと対話し、その過程でたくさんの無害な間違いを犯したいのです。
タイプチェッカー のエラーメッセージは、開発者が型システムの論理を誤解したり、あるいは適用を誤ったりしたときに開発者を助けようとするもので、本書では間接的に説明されています。
これらのエラーメッセージは時間の経過とともに改善されていくため、このドキュメントでは特定のエラーメッセージを記載していません。 その代わりに、各コード例をその周辺の文章で説明するようにしています。
Motoko 標準ライブラリの使い方
言語のエンジニアリングにおけるさまざまな実用上の理由から、Motoko の設計では組み込みの型や操作を最小限に抑えるようにしています。
その代わり、Motoko 標準ライブラリは、言語を完全なものにするための型や操作を可能な限り提供しています。 *ただし、この標準ライブラリは開発中であり、まだ不完全です。*
Motoko 標準ライブラリでは、Motoko 標準ライブラリからのモジュールを 選定して 示していますが、これは例題で使われているコアな機能に焦点を当てたもので、抜本的な変更はないと考えられます。 しかし、これらの標準ライブラリの API はすべて時間の経過とともに(程度の差こそあれ)確実に変化し、特にサイズと数が大きくなっていくでしょう。
標準ライブラリからインポートするには、import
キーワードを使います。 導入するローカルモジュールの名前、この例では “Debug” を表す D
と、import
宣言がインポートされたモジュールを見つけるための URL を指定します。
import D "mo:base/Debug";
D.print("hello world");
ここでは、Motoko のコードを(他のモジュール形式ではなく)、mo:
という接頭辞でインポートします。 base/
のパスを指定し、その後にモジュールのファイル名 Debug.mo
から拡張子を除いたものを指定します。
値の出力
上の例では、ライブラリ Debug.mo
の関数 print
を使ってテキスト文字列を出力しています:
print: Text -> ()
print
関数は、入力として(Text
型の)テキストの文字列を受け取り、出力として(ユニット型 または ()
の) ユニット値 を生成します。
ユニット値は情報を持たないので、ユニット型の値はすべて同一であり、print
関数は実際には何の結果も生み出しません。結果の代わりに 副作用 を伴います。 print
関数は、人間が読める形式のテキスト文字列を出力端末に出力するという効果があります。出力したり、ステートを変更したりするような副作用のある関数は、しばしば 非純粋関数 と呼ばれます。一方、副作用を伴わずに値を返すだけの関数は、純粋関数 と呼ばれます。 返り値(ユニット値)については以下で詳しく説明し、void
型のコンセプトに慣れている読者のために、void
型との関連性についても述べます。
最後になりますが、ほとんどの Motoko の値はデバッグ用に人間が読めるテキスト文字列に変換することができ、それらの変換を自分で書く必要は ありません 。
debug_show
プリミティブは、大規模なクラスの値を Text
型の値に変換することができます。
例えば、((Text, Nat, Text)
型の)トリプルを、独自の変換関数を自分で書かずに、デバッグ用のテキストに変換することができます:
import D "mo:base/Debug";
D.print(debug_show(("hello", 42, "world")))
これらのテキスト変換を使用して、プログラムを試す際にほとんどの Motoko データを出力することができます。
不完全なコードへの対応
プログラムを書いている最中に、完成前のコードや、いくつかの実行パスが見つからないか無効な状態のコードを実行したいと思うことがあります。
このような状況に対応するために、標準ライブラリである Prelude
の xxx
, nyi
, unreachable
関数を、後述のように使用することができます。 それぞれの関数は、以下に説明する 一般的なトラップメカニズム をラップしています。
短期的な穴を埋める
プログラム上に開いた短期的な穴(式の欠落)は、ソースリポジトリにコミットされることはなく、まだプログラムを書いている開発者の開発セッションでのみ存在します。
次のように Prelude をインポートしていたと仮定します:
import P "mo:base/Prelude";
開発者は、次のようにして 欠けている式 を埋めることができます:
P.xxx()
その結果、この式が実行された場合には、コンパイル時に 常に 型チェックが行われ、実行時に 常に トラップが行われます。
長期的な穴を文書化する
慣習的に、長期的な穴は「まだ実装されていない」(nyi: not yet implimented
)機能とみなされ、Prelude モジュールの似たような関数を使ってマークすることができます。
P.nyi()
到達不可能な
コードパスを文書化する
上記の状況とは対照的に、プログラムの不変条件における内部論理の一貫性を仮定すると、 コードが評価されることが 決してない ので、コードは 決して埋められない という場面もあります。
コードパスを、論理的に不可能あるいは 到達不可能 なものとして記録するには、標準ライブラリの関数である unreachable
を使います:
P.unreachable()
上記の状況と同様に、この関数はいかなる前後関係でも型チェックを行い、評価されるといかなる前後関係でもトラップを行います。
実行の失敗によるトラップ
ゼロ除算、配列の範囲外へのアクセス、パターンマッチの失敗などのエラーは型システムでは防ぐことができませんが、実行時に トラップ と呼ばれるエラーを引き起こす可能性があります。
1/0; // ゼロ除算によるトラップ
let a = ["hello", "world"];
a[2]; // 配列の範囲外へのアクセスによるトラップ
let true = false; // パターンマッチの失敗
コードの実行がトラップを引き起こしたとき、コードが トラップした と言います。
コードの実行は最初のトラップで中断され、以降は実行されません。
Actor メッセージ内で発生するトラップは少し微妙です。Actor 全体を中止するのではなく、特定のメッセージの進行を妨げ、まだコミットされていないステートの変更をロールバックします。Actor 上の他のメッセージは実行を継続します。
明示的なトラップ
ときどき、ユーザーが定義したメッセージを用いて、無条件にトラップを強制することが有用な場合があります。
Debug
ライブラリでは、この目的のために、trap(t)
関数を提供しており、どのような文脈においても使うことができます。
import Debug "mo:base/Debug";
Debug.trap("oops!");
import Debug "mo:base/Debug";
let swear : Text = Debug.trap("oh my!");
(前述の Prelude
関数の nyi()
、unreachable()
、xxx()
は、Debug.trap
の単純なラッパーです。)
アサーション
アサーションでは、あるブール値のテストが成立しなかったときに条件付きでトラップし、成立する場合は実行を継続することができます。例えば、以下のようになります:
let n = 65535;
assert n % 2 == 0; // n が偶数ではない場合にトラップ
assert false; // 無条件にトラップ
import Debug "mo:base/Debug";
assert 1 > 0; // トラップしない
Debug.print "bingo!";
アサーションが成功して実行に移ることもあるため、()
型の値が期待される文脈でのみ使用することができます。