このページの内容

モジュール化

小さなおもちゃ以上のプログラムを書き始めると、すぐに二つの問題に遭遇します。

  1. 全体を把握しやすいように、複数の小さなファイルへ分割したい。

  2. その一部分を、別のプログラムでも再利用したい。

これらに対応するため、Wrenには単純なモジュールシステムがあります。Wrenのコードを含むファイルは、モジュールを定義します。モジュールは、別のモジュールをインポートすることで、そのモジュールで定義したコードを使えます。大きなプログラムを、インポートする小さなモジュールへ分割でき、複数のプログラムで一つのモジュールを共有することで、コードを再利用できます。

Wrenには、単一のグローバルスコープがありません。代わりに、各モジュールが、ほかのすべてのモジュールから独立したトップレベルのスコープを持ちます。例えば、二つのモジュールで同じ名前のトップレベル変数を定義しても、名前の衝突が起きません。各モジュールは、モジュールとして独立しています。

インポートの概要 #

Wrenを実行し、実行するファイル名を渡すと、そのファイルの内容が、実行を開始する「main」モジュールを定義します。ほかのモジュールを読み込んで実行するには、import文を使います。


import "beverages" for Coffee, Tea

これは、「beverages」というモジュールを探し、そのソースコードを実行します。続いて、そのモジュール内の二つのトップレベル変数、CoffeeとTeaを探し、それらの値を持つ新しい変数をこのモジュールに作成します。

この文は、変数宣言が許される場所なら、ブロック内を含め、どこにでも書けます。


if (thirsty) {
  import "beverages" for Coffee, Tea
}

別の名前で変数をインポートする必要がある場合は、import "..." for Name as OtherNameを使えます。これは、そのモジュールのトップレベル変数Nameを探し、その値を持つOtherNameという変数をこのモジュールに宣言します。


import "liquids" for Water //Water is now taken
import "beverages" for Coffee, Water as H2O, Tea
// var water = H2O.new()

モジュールを読み込むだけで、その変数を束縛する必要がない場合は、for節を省けます。


import "some_imperative_code"

これが基本的な考え方です。では、行う処理を各段階に分けましょう。

  1. モジュールのソースコードを見つける。
  2. インポートするモジュールのコードを実行する。
  3. インポートする側のモジュールに新しい変数を作り、インポートされるモジュールで定義した値に束縛する。

各段階を説明します。

モジュールを見つける #

モジュールをインポートするときに、最初に必要なのは、そのコードを実際に見つけることです。importは、モジュールを一意に識別するための任意の文字列である名前を指定します。組み込み先のアプリケーションが、その文字列からソースコードのまとまりを見つける方法を制御します。

ホストアプリケーションは、新しいWren VMを作るときに、モジュールローダー関数を提供します。


WrenConfiguration config;
config.loadModuleFn = loadModule;

// Other configuration...

WrenVM* vm = wrenNewVM(&config);

その関数のシグネチャは、次のとおりです。


WrenLoadModuleResult WrenLoadModuleFn(WrenVM* vm, const char* name);

モジュールをインポートするたび、VMは、この関数へモジュール名を渡して呼び出します。組み込む側は、モジュールのソースコードの内容を、WrenLoadModuleResultで返すことを求められます。自分のアプリにWrenを組み込む場合、ファイルシステムを参照したり、アプリに同梱した資源から探したりと、好きな方法で処理できます。

この関数から、sourceフィールドをNULLとして返すと、モジュールが見つからなかったことを示せます。この場合、Wrenは実行時エラーとして報告します。

コマンドラインのローダー #

Wren CLIコマンドラインツールの検索処理は、とても単純です。mainモジュールを読み込んだディレクトリーに、モジュール名と「.wren」を追加し、そのファイルを探します。例えば、次のように実行したとします。

$ wren code/my_program.wren

そのmainモジュールに、次の文があると、


import "some/module"

コマンドラインVMは、/code/some/module.wrenを探します。慣例として、スクリプトをプラットフォームに依存させないため、Windowsでも、パスの区切りにはスラッシュを使うべきです。(Windowsでもスラッシュは有効な区切りですが、ほかのOSではバックスラッシュは有効ではありません。)

モジュールを実行する #

モジュールのソースコードが得られたら、それを実行する必要があります。まずVMは、インポートする側のモジュールでimport文を実行しているファイバーを一時停止します。

続いて、新しいモジュールオブジェクト、つまり基本的には新しいトップレベルのスコープと、新しいファイバーを作成します。そのファイバーとスコープで、新しいモジュールのコードを実行します。モジュールは、任意の命令型コードを実行し、任意のトップレベル変数を定義できます。

モジュールのコードの実行が終わり、そのファイバーが完了すると、一時停止していた、インポートする側のモジュールのファイバーを再開します。この中断と再開は再帰的です。「a」が「b」をインポートし、「b」が「c」をインポートするなら、「c」の実行中は「a」と「b」の両方を中断します。「c」が終わると「b」を再開し、その後「b」が完了すると「a」を再開します。

インポートの木を、一度に一つのノードずつたどるように考えてください。どの時点でも、実行中のコードは、一つのモジュールのものだけです。

変数の束縛 #

モジュールの実行が終わると、最後の段階は、そこからデータを実際にインポートすることです。どのモジュールでも、「トップレベル」の変数を定義できます。これは、メソッドや関数の外で宣言する変数のことです。

これらは、モジュール内のどこからでも見えますが、エクスポートして、ほかのモジュールから使うこともできます。Wrenが次のようなimportを実行すると、


import "beverages" for Coffee, Tea

まず、「beverages」モジュールを実行します。続いて、for節の各変数名を順に調べます。各名前について、インポートされたモジュールで、その名前のトップレベル変数を探します。変数が見つからなければ、実行時エラーになります。

見つかった場合は、その変数の現在の値を取得し、同じ名前と値を持つ新しい変数を、インポートする側のモジュールに定義します。インポートする側が得るのは独自の変数で、その値は、インポートした時点の元の変数の値のスナップショットです。後で一方のモジュールがその変数に代入しても、もう一方には反映されません。「生きた」接続ではありません。

実際には、トップレベル変数の大半は、一度しか代入しないため、この違いが問題になることはまれです。

共有されるインポート #

先ほど、プログラムのモジュールの集合を木として説明しました。もちろん、モジュールが木になるのは、共有されるインポートがない場合だけです。次のようなプログラムを考えてください。


// main.wren
import "a"
import "b"

// a.wren
import "shared"

// b.wren
import "shared"

// shared.wren
System.print("Shared!")

ここでは、「a」と「b」の両方が「shared」を使おうとします。「shared」がトップレベルの状態を定義するなら、メモリー内に、そのコピーは一つだけあってほしいところです。そのため、モジュールのコードを実行するのは、最初に読み込むときだけです。その後、同じモジュールを再びインポートすると、すでに読み込んだモジュールを探すだけです。

内部では、Wrenは、これまで読み込んだすべてのモジュールのマップを保持します。モジュールをインポートするとき、ソースを得るために組み込む側を呼び出す前に、まず、そのマップで探します。

つまり、上の段階の一覧には、暗黙の第ゼロ段階として、「すでにモジュールを読み込んだか調べ、読み込み済みなら再利用する」があります。したがって、上のプログラムが「Shared!」を表示するのは一度だけです。

循環するインポート #

少し注意すれば、インポートを循環させることもできます。読み込み処理の詳細は、次のとおりです。

  1. 指定した名前のモジュールを、すでに作成したか調べる。
  2. 作成済みなら、それを使う。
  3. そうでなければ、その名前の新しいモジュールを作り、モジュールの登録簿に保存する。
  4. そのためのファイバーを作り、コードを実行する。

最後の二段階の順序に注意してください。モジュールを読み込むときは、実行する前に登録簿へ追加します。これにより、そのモジュール自身や、そのモジュールがインポートしたものの実行中に、同じモジュールのimportへ到達しても、登録簿で見つかり、循環を打ち切れます。

例えば、次のようになります。


// main.wren
import "a"

// a.wren
System.print("start a")
import "b"
System.print("end a")

// b.wren
System.print("start b")
import "a"
System.print("end b")

このプログラムは正常に実行し、次のように表示します。

start a
start b
end b
end a

注意が必要なのは、変数の束縛です。次の例を考えてください。


// main.wren
import "a"

// a.wren
import "b" for B
var A = "a variable"

// b.wren
import "a" for A
var B = "b variable"

ここでは、b.wren内で「a」をインポートすると失敗します。実行をたどると、次のようになります。

  1. 「main.wren」でimport "a"を実行する。「main.wren」を中断する。
  2. 「a.wren」でimport "b"を実行する。「a.wren」を中断する。
  3. 「b.wren」でimport "a"を実行する。「a」はすでにモジュールのマップにあるため、「b.wren」は中断しない。

代わりに、そのモジュールでAという変数を探します。しかし、「a.wren」は宣言より前のimport "b" for Bの行で停止しているため、その変数はまだ定義していません。動作させるには、変数宣言をimportより前へ移動する必要があります。


// main.wren
import "a"

// a.wren
var A = "a variable"
import "b" for B

// b.wren
import "a" for A
var B = "b variable"

今度は、実行すると次のようになります。

  1. 「main.wren」でimport "a"を実行する。「main.wren」を中断する。
  2. 「a.wren」でAを定義する。
  3. 「a.wren」でimport "b"を実行する。「a.wren」を中断する。
  4. 「b.wren」でimport "a"を実行する。「a」はすでにモジュールのマップにあるため、「b.wren」は中断しない。すでに定義済みのAを探し、それを束縛する。
  5. 「b.wren」でBを定義する。
  6. 「b.wren」を完了する。
  7. 「b.wren」でBを探し、「a.wren」に束縛する。
  8. 「a.wren」を再開する。

非常に複雑に聞こえますが、それは循環依存が一般に複雑なためです。ここでの要点は、必要になるまれな場合に、Wrenは循環依存を扱うことができるということです。



← エラー処理