Erlangプログラムの基本
Erlangプログラムは、0位置にダッシュ文字(-)を使う宣言で始まり、この宣言で関数の名前と可用性と定義を示します。宣言の例を2つ見てみましょう。
-module(fact).
モジュール
Erlangはモジュールを使います。モジュールfactはfact.erlというファイルにあり、その先頭にはモジュール宣言が必要です(ここ(zipファイル)をクリックするとファイルをダウンロードできます)。ご覧のように、すべての文はドット(.)で終了します。
-export([start/2]).
それぞれのモジュールに多数の関数を記述できますが、モジュールの外から参照したり呼び出したりできるのはエクスポートされた関数だけです。start/2の部分は、エクスポートされる関数の名前がstartで、2つの引数を受け取ることを示します。
start(ProcNum, Number) ->
ComputationStart = erlang:now(),
...
関数はこのように定義します。関数定義全体が1つの文であり、関数内の各命令はカンマ(,)で区切ります。
変数
変数の先頭は大文字にします。潜在的な問題を回避するため、変数に定数値をバインドできるのは1回だけです。変数はバインドしていなくてもかまいませんが、バインドしていない変数があるとコンパイラから警告が出ます。
Erlangの変数の型はわずかです。最もよく使うのは、数値、アトム、リスト、タプルです。アトムは定数値で、名前の先頭は小文字にします。小文字以外で始める場合は名前を引用符で囲みます。リストとタプルは複数個の数値、アトム、リスト、タプルを格納し、リストの要素数は可変、タプルの要素数は固定です。リストは大かっこで、タプルは中かっこで囲みます。Erlangは動的型指定言語なので、実行時に変数の型チェックが実行されます。
モジュールからエクスポートされている関数を呼び出すErlangの構文の例を以下に示します。ここでは、モジュールlistsの関数seqが、1~Numberの整数をすべて含むリストを返します。Erlangの基本インストールには、listsなどのユーティリティモジュールのセットが提供されています。
compute(ProcNum, lists:seq(1,Number)),
computeという関数の呼び出しに注目してください。この関数は次のように定義されています。
compute(_, Numbers) when length(Numbers) == 1 ->
...
compute(ProcNum, Numbers) when ProcNum == 1 ->
...
compute(ProcNum, Numbers) ->
...
1つの関数を複数の方法で定義できます。この例では、この関数の3種類の定義が示されており、ガード節の部分が異なっています。さらにコードを見てみると、入力引数の数や種類を変えて同じ関数を定義できることも分かります。
再帰とパターンマッチング
以下のコードは、関数create_intervalsの定義です。この関数は、1~nの数の区間全体を、後で並列プロセスで処理するための複数の小さな区間に分割します。
create_intervals(ListLen, Delta) when ListLen =< Delta ->
...
create_intervals(ListLen, Delta) ->
create_intervals(Delta + 1, ListLen, Delta, [[1, Delta]]).
create_intervals(StartIndex, ListLen, Delta, L) when ... ->
...
create_intervals(StartIndex, ListLen, Delta, L) ->
create_intervals(...).
この場合、forループを使うのが自然に思えますが、Erlangでは反復の代わりに再帰を使用します。従って、create_intervalsは自分を呼び出して再帰的にリストのリストを作成します。それぞれのリストの2つのアトムは、分割した区間のインデックスの極値を表します。
先ほどのcompute関数を見てみましょう。この関数は、数のリストをグループに分け、グループごとの積を計算し、それらの積のリスト(元のリストより短くなる)を返します。このプロセスの最後にこれを再帰的に実行すると、元のリスト内の数の積が返されます。従って、compute関数の最終行は次のようになります。
compute(ProcNum, Products).
一方、関数create_intervalに戻ると、ガード節と入力パターンの異なる、複数の定義があります。
Erlangでは、呼び出す関数の適切な選択と変数代入の両方にパターンマッチングが徹底的に使われます。演算子=も「イコールトゥ」と読まずに「マッチトゥ」と読み、2つのものが形状的にも等しいことを示しています。微妙な違いとはいえ、これはErlangのコードを読んだり書いたりするうえで留意すべき重要な考え方です。例えば、以下のコードを入力するとします。
A = 1.
この場合、システムは値Aを1と一致させようとします。システムでAを最初に使うときは、マッチングは成功し、変数Aは1にバインドされます。それ以降は、Aは1としかマッチングに成功しなくなり、それ以外のマッチングや代入はすべて失敗します。
以下の例を見れば、パターンマッチングの能力を予見できます。
L = [a, b, c, d]. [A | B] = L.
記号|は、リストのヘッド(先頭の項目)とテイル(ヘッド以外の項目)を分けます。したがって、この文では、パターンマッチングによって変数Aがアトムaに、Bがリスト[b, c, d]にバインドされます。
また、以下のコードの場合、
[C, D | E] = L.
Cはaに、Dはbに、Eはリスト[c,d]にバインドされます。
並列プロセスの作成
Erlangの並列処理サポートを使うと、対称型マルチプロセッサ(SMP)アーキテクチャとマルチコアCPUを活用できます。実際、Erlangアプリケーションのデザインではプロセスを主体にします。この場合の並列性は、ホストオペレーティングシステムではなくErlangから提供されます。ただし、Erlangプロセスは、ネイティブスレッドとグリーンスレッドの中間にあたり、Erlang Run Time System(ERTS)で生成されますが、他のプロセスとステートを共有しません。
compute関数内の以下の行は、Erlangで並列プロセスを作成する方法を示しています。
ProductsCollector_PID = spawn(fact, products_collector, [[], self()]),
関数spawn/3を使うと、関数を開始したプロセスと並列の、別のプロセスを生成できます。第1パラメータはプロセスを生成するモジュール、第2パラメータは並列に実行する関数とそのパラメータのリストです。この行で返されるのは、新しく生成されたプロセスのPIDにバインドされる特殊な変数、PID変数です。この値は、新しいプロセスとのやり取りに必須の重要な値です。
図1のフローチャートを見るとわかるように、関数computeからは、関数products_collectorと、関数partial_computeの複数のインスタンスが、並列に生成されます。partial_computeの各インスタンスは、それぞれの数の区間(元の区間を分割した区間)の積を計算します。
partial_computeの各インスタンスが各区間の数の積を求めるのに要する時間は予測できません。そのため、products_collectorというもう1つのプロセスが必要になります。このプロセスは、partial_computeの各インスタンスが完了ししだい、それぞれの結果を受け取り、すべてのpartial_compute関数の終了を検出します。
partial_computeの複数インスタンスは以下のようにして生成します。
lists:map(fun(X) ->
spawn(fact, partial_compute, [X,Numbers,ProductsCollector_PID])
end,
Intervals),
関数lists:mapには引数が2つあります。第2引数はリスト、第1引数はそのリストの要素に適用する関数のインライン定義です。lists:map関数は結果を格納した別のリストを返します。このように、Erlangでは、関数の引数として他の関数の名前を渡すことができます。
