Erlang Land

電話会社エリクソンの研究所で作られた言語「Erlang」についてしばらく調べてみようと思います。読み方はたぶん"あーらん"で。

リアルタイム処理や誤り検出が可能で組込用途に使われていて、並行処理の記述も可能、 ML や Haskell、Clean などに代表される関数型プログラミング言語の一種である、 と、見た目なかなか盛りだくさんな感じ。さてさて。

ご意見・間違いの指摘等大歓迎。

  1. インストール
  2. 概観1
  3. 概観2
  4. * 変数
  5. 構文の基本
  6. データ構造
  7. * パターンマッチ1
  8. パターンマッチ2
  9. パターンマッチ3
  10. 関数定義
  11. 落ち穂拾い
  12. メモ
  13. プロセス1
  14. * プロセス2

インストール (2003/04/09)

Open Source Erlang のページから、 ソースやメジャーなOS用のバイナリが手に入ります。異様にデカいので、 回線の細い方には厳しいかもしれません。Windowsの場合、 落としたインストーラを普通に実行すればインストール完了です。 起動はスタートメニューから。または、パスを通せば erl.exe でコマンドラインからシェルの起動もできます。

Documentation のページに幾つかチュートリアル的な資料あり。言語仕様も下の方に ps で提供されています。

概観 (2003/04/12)

werl、または erl を起動すると、対話型インタプリタが起動します。 ここにerlangの式を打ち込むことで、簡単な計算を行うことができます。

1> 1 + 2.
3
2> -1.23 * 4 - 5.
-9.92000
3> 57129857135719571290578195712057120 * 539571395731957329572395.
30825636752687531077972959887014581565989488985235415202400
4> 3 / 2.
1.50000

式の最後に.(ピリオド)を打ってEnterキーを押すことで、 式が評価され結果が表示されます。多倍長も何のその。 C言語などに慣れている方は、3/2が1とならないことに注意。

5> math:sin(1.57).
1.000000
6> math:pow(2, 5).
32.0000

関数の呼び出し方。関数名( カンマで区切った引数リスト ) となります。普通。math:sin は「mathモジュールのsin関数」 を表しています。この辺りの詳細はのちのち。

7> io:put_chars("Hello, World\n").
Hello, World
ok

概観2 (2003/04/18)

対話型インタプリタ上でなく、ファイルへソースを書いて、 それをインタプリタへ喰わせてみます。あとついでに、関数の書き方。

-module(sample).
-export([square/1]).

square(X) -> X * X.

これをsample.erlという名前で保存しておいて、

1> c(sample).
{ok,sample}
2> sample:square(5).
25

とします。上から順に解説すると、「このファイルはモジュールsampleの定義です」 「モジュールからはsquareという1変数関数をexportします」 「square関数は X を取って X * X を返します」で、 「sample.erlファイルを読み込み」「sampleモジュールのsquare関数を引数5で呼出」 ですね。

erlangはJavaScriptなどと同じで型付けの弱い言語なので、 squareの引数の型などを明示的に指定する必要はありません。

変数 (2003/04/26)

2週間かけてようやく三回目を書いているとは何事か! と自分を叱りつつ久々の更新です。

1> X=1.
1
2> Five=5.
5
3> _result = X + Five.
6
4> _result.
6

変数名としては、「大文字か_(アンダースコア)で始まるものだけ」 を使うことができます。小文字始まりはダメ。変数名 = 式.と書くことで、式の結果を変数に入れることができます。 ただし、「代入」と言うとやや語弊があるので注意。というのは、 一度変数へ値を束縛してしまったら、別の値を入れ直すことができません。

1> X = 20.
20
2> X = 10.
** exited: {{badmatch,10},[{erl_eval,expr,3}]} **

今のところエラーメッセージがワケわかりませんが、いずれ解説します。 (簡単に言うと、20と10は値が違うからbadmatchだよー、と言ってます。 2回目の左辺のXは、既に20という値として評価されてしまう。)

書き換えない変数

すでにMLやHaskellなどをご存じの方はこのsectionは飛ばして下さい。^^;

変数の値を途中で変えられないのって困らない? と疑問をお持ちの方もおられるかもしれません。 例えば次のようなC言語の関数を考えてみましょう。0からnまでの和を計算。

int sum(int n)
{
	int i, s=0;
	for(i=0; i<n; i++)
		s += i;
	return s;
}

ループしながら、sやiの値を何度も書き換えて使っています。が、これは

int sum(int n)
{
	if( n==0 ) return 0;
	else       return n + sum(n-1);
}

再帰を使ってこう書いてしまえば、変数書き換えの必要はなくなりました。 これは一番簡単な例ですが、基本的にたいていの処理はすぐこういう形に書けます。 ので、あまり困りません、という答えになります。

変数宣言?

枝葉末節なので、プログラムの意味論、 とかに興味のある方以外はこのsectionは飛ばして下さい。^^;

かと言って、代入ではなく変数宣言、と言ってしまうのも少し違うそうです。 言語仕様書にて、Erlang is different from most other programming languages -- including other functional programming language -- と始まる文で、 他の言語は「expression(式の値を得るために評価される)」と「command (作用を得るために実行される)」は区別しないが、「declaration」 は別物として扱っている、と書かれています。逆に言えばこの言語はそうではない、 ということでしょう。

細かい話なのでどーでもいいのですが、[左辺] = [右辺] は、 「両辺が等しくなるように、環境({変数,値}の割り当てリスト)を拡張する」 を意味する感じの式であるとされています。

基本的な構文の基本 (2003/04/26)

関数定義

-module(sample).
-export([double/1, add/2]).

double(X) -> X * 2.
add(X, Y) -> X + Y.

X を受け取って X*2 を返す関数 double と、 X,Yの2つを受け取って X+Y を返す関数 double を書いてみました。 「関数名(仮引数リスト) -> 関数本体.」です。

return文のような特別な物は存在しません。というのは、 この言語には「文」が存在せず、全てが「式」、 つまり値を持つ構造で成り立っているからです。 なので、常に"関数全体の式"は常に値をもつ(上の例ではX*2の値、とかX+Yの値) ため、この値を関数から返す、と簡単に決められているようです。 下で見るように、if なども全て値を返します。

関数適用

1> c(sample).
{ok,sample}
2> sample:double(100).
200
3> sample:add(30, -20).
10

モジュール名:関数名(引数リスト)」ということで。普通です。

if

さっきのsum関数をerlangで書いてみましょう。

sum(X) ->
	if
	  X==0 -> 0;
	  X/=0 -> X + sum(X-1)
	end.

==/= は値が等しい/等しくない ならtrueを返します。

if 条件1 -> 式1; 条件2 -> 式2; ...; 条件n -> 式n end」 という形で、条件は何個でも並べることができて、先に書いた条件ほど優先されます。 elseに当たるものはないので、上のように否定を条件にするか、 true-> と書くか…しかないようです。

f(X) ->
	io:put_chars(
	  if X rem 6 == 0 -> "6の倍数\n";
	     X rem 3 == 0 -> "3の倍数\n";
	     X rem 2 == 0 -> "2の倍数\n";
	     true         -> "どれでもない\n"
	  end
	).

実際に実行された式の値が、if~end 全体の値とされます。

case

f(X) ->
	io:put_chars(
	  case X of
	    1 -> "hogehoge";
	    2 -> "fugafuga";
	    3 -> "foobar";
	    _ -> "zzz..."
	  end
	).

X の値によって分岐。CやJavaのswitchみたいに見えますが、 case式ではより強力な「パターンマッチ」ができます。後述。

Body

1> A=1, B=2, A+B.
3

,で区切って複数の文を並べると、左から右へ実行されます。

fun

1> F = (fun (X,Y) -> X+Y end).
#Fun<erl_eval.11.1870983>
2> F(1,2).
3
3> (fun (X,Y) -> X+Y end)(1, 2).
3

fun~end で無名関数。

データ構造 (2003/04/27)

list

1> [1,2,3,4,5].
[1,2,3,4,5]
2> ["aiueo", 3.5, [1,2], []].
["aiueo",3.50000,[1,2],[]]

[...] で「リスト」を作ることができます。リストの要素には、 上の例のように何でも入ります。[]で空リスト。

3> [1,2,3] ++ [4,5].
[1,2,3,4,5]
4> [1,2,3] -- [2,3].
[1]

リストをくっつけたり後ろから除いたり、の演算が ++-- として用意されています。

5> [1|[2,3,4]].
[1,2,3,4]

あるいは、[値1 | リスト] で、 先頭に値1を入れた新しいリストになります。

tuple

1> {1,2,3}.
{1,2,3}
2> {"abc", {2.5, []}, fun (X)->X+X end}.
{"abc",{2.50000,[]},#Fun<erl_eval.5.123085357>}

{...}で「タプル(組)」です。 こちらもまた要素としては何でも入ります。

何だか上の例だとリストとタプルの違いがわかりにくいですけど、 リストは要素の個数が一定でないときに使い、 タプルは要素の個数が一定な物を表すときに使います。 例えば2次元の多角形を表すことを考えると、 点の座標にはtupleを使って、多角形の頂点を表すにはlist。 [{0,0} {2,1}, {-4,3}] で三角形とかですね。

record

構造体。名前が付きフィールドの集まり。

-module(sample).
-export([make_square/1]).

-record( rectangle, {x,y,width,height} ).

make_square(Size) ->
	#rectangle {x=0, y=0, width=Size, height=Size}.
1> c(sample).
{ok,sample}
2> sample:make_square(10).
{rectangle,0,0,10,10}

-record( レコード名, {フィールド名,...} ). で宣言して #レコード名 {フィールド名=式,...} で作成します。 で、上の例の返値をよく見るとわかるように、recordは実は単なる tuple の SyntaxSuger として実現されています。

-module(sample).
-export([make_square/1, area/1, setwidth/2]).
-record( rectangle, {x,y,width,height} ).

make_square(Size) ->
	#rectangle {x=0, y=0, width=Size, height=Size}.

area(R) ->
	(R #rectangle.width) * (R #rectangle.height).

setwidth(R, W) ->
	R #rectangle { width=W }.
1> c(sample).
{ok,sample}
2> X = sample:make_square(6).
{rectangle,0,0,6,6}
3> Y = sample:setwidth(X, 10).
{rectangle,0,0,10,6}
4> sample:area(Y).
60
5> sample:area(X).
36

area関数のように、式 #レコード名.フィールド名 と書くことでフィールドの値を取り出せます。

また、 変数の書き換えがないのと同じで基本的に値を"書き換える"ことはしない言語なので、 フィールドへ書き込む代わりに、変えたいところだけ変えた新しいレコードを返す、 という操作が備わっています。setwidth関数のように、 式 #レコード名{フィールド名=値} と書きます。

データ構造・補足 (2003/04/27)

-record( rectangle, {x=0,y=0,width,height} ).
make_square(Size) ->
	#rectangle {width=Size, height=Size}.

デフォルトの初期化値を指定しておくことで、 作成時に省略したらその値が使われるようにできます。

パターンマッチ (2003/04/28)

とりあえず例

1> A = {1,2,3}.
{1,2,3}
2> A.
{1,2,3}

変数 A に {1,2,3} という tuple を束縛しています。ここまでは普通。

3> {B,C,D} = A.
{1,2,3}
4> B.
1
5> C.
2
6> D.
3

{B,C,D} と A が等しくなるように、B,C,Dに適切な値を束縛します。 Aは{1,2,3}なので、ここではBが1、Cが2、Dが3になります。

7> {E,F} = A.
** exited: {{badmatch,{1,2,3}},[{erl_eval,expr,3}]} **

{E,F} と {1,2,3} を合わせるのはチト無理があります。 ということで "badmatch" のエラー。

...というように

[左辺] = [右辺] の式で、右辺のデータ構造を見て、 左辺の未定義変数を適切に束縛していく…という処理をパターンマッチといいます。 上の例では単純なtupleでしたが、recordやlistでも勿論、tupleのtupleとか、 listのtupleのtupleのlist、みたいな複雑な構造でもOK。

8> [X,Y,Z] = [1,2,3].
[1,2,3]
9> Y.
2

リストの場合。

10> [H|T] = [1,2,3,4].
[1,2,3,4]
11> H.
1
12> T.
[2,3,4]

[リストの先頭 | リストの残り] と分離させることができます。 この分け方は関数の再帰でリスト処理をする際に非常に都合がよいので、 関数型言語では定番になっています。Erlangでも、 リスト処理の基本パターンはこちらの方で、[X,Y,Z] みたいに , で区切る方がむしろ SyntaxSuger です。

少し複雑な例

-record( rectangle, {x,y,width,height} ).

area(R) ->
	#rectangle{width=W,height=H} = R,
	W * H.

左辺にレコードパターンを書いていつも通り = で繋ぐとOkです。 (注) 変数XとかYとかを再利用したいので、 私はここで一旦インタプリタ再起動しました。以下の例で命令番号が 1 に戻ってる点ではそのたび再起動してます。ご注意を。(注終)

1> {[X|Y], Z, {A,B}} = {[1,2,3,4], 5, {6,7}}.
{[1,2,3,4],5,{6,7}}
2> Y.
[2,3,4]
3> A.
6

入り組んだ構造でも特に問題なく。

1> K = {"hoge", [], {1,2,3}}.
{"hoge", [], {1,2,3}}
2> {S,T,U} = K.
{"hoge",[],{1,2,3}}
3> S.
"hoge"
4> T.
[]
5> U.
{1,2,3}

さっきから、最終的に変数と数値がマッチする例ばかりやってますが、 別に変数と文字列でも、変数とタプルでも構いません。

パターンマッチ2 (2003/04/28)

ユニバーサルパターン

元のデータの一部だけ取り出したいとき、などにもパターンマッチは有効です。 例えばリストを指している変数Lから、 そのリストの先頭要素だけを取り出してみましょう。

1> L = [1,2,3].
[1,2,3].
2> [Head|T] = L, Head.
1

しかしこの例、先頭しか要らないのに残りのために T という変数を使っているのは何だか無駄ですね。この場合、

3> [Head2|_] = L, Head2.
1

_ (下線) を書いておくことで、「特に変数に束縛はしないけど、 そこではうまくマッチしたことにする」ことができます。

定数とマッチ / badmatch

1> {X, 1} = {15, 1}.
{15, 1}.
2> {Y, 2} = {15, 1}.
** exited: {{badmatch,{15,1}},[{erl_eval,expr,3}]} **

{X,1}と{15,1} は Xを15にすれば等しくなりますが、{Y,2}では Y をどうしても無理です。ので、badmatchエラー。 こんな風に、左辺には変数だけでなくて定数値もおくことができます。 これができると何故嬉しいか、については次の節で紹介しますので、 今はしばしお待ちを。

変数は書き換えられない

1> X = 1.
1.
2> {X, Y} = {2, 3}.
** exited: {{badmatch,{2,3}},[{erl_eval,expr,3}]} **

2命令目の時点ではもう、Xは1 と決まっちゃってます。 なので、このパターンマッチでは X はもう変数でなく、 ただの値 1 として扱われます。結果、1と2のbadmatch。

3> {A,A} = {1,1}.
{1,1}.
4> {B,B} = {2,3}.
** exited: {{badmatch,{2,3}},[{erl_eval,expr,3}]} **

3>の例は、タプルのどっちかで A=1 とまず決まって、 するともう片方が 1 と 1 のマッチになるのでめでたく成功します。 対して4>の例は、評価順序によってB=2B=3 のどっちかにまず決まって、次に2と3のbadmatchエラーが発生、と。

パターンマッチ3 (2003/05/01)

case式はこんな使い方ができます。

-module(sample).
-export([judge/1]).

judge(X) ->
	case X of
		[]      -> "empty list";
		[A|_]   -> {"nonempty list with head", A};
		{A}     -> {"single element tuple", A};
		{A,B}   -> {"double element tuple", A,B};
		{A,B,C} -> {"triple element tuple", A,B,C};
		_       -> "otherwise"
	end.
1> c(sample).
{ok,sample}
2> sample:judge( [1,2,3] ).
{"nonempty list with head",1}
3> sample:judge( {-1, -2} ).
{"double element tuple",-1,-2}
4> sample:judge( [] ).
"empty list"
5> sample:judge( 100 ).
"otherwise"

パターンを並べて置いて、最初にマッチした部分の本体を実行、と。 だいぶ上の方で書いた case X of 1 -> ...; 2-> ... の例も単に、"定数パターン" を並べているだけ、と捉えることができます。

しかし、上のjudgeのような 「リストかタプルか何だかよくわからんものを受け取って何かする関数」 なぞを書きたい人、は普通あまりいないと思われますので、 もう少し有益な応用を考えてみましょう。

例1

-module(sample).
-export([reverse/1]).

reverse(List) ->
	case List of
		[]          -> [];
		[Head|Tail] -> reverse(Tail) ++ [Head]
	end.
1> c(sample).
{ok,sample}
2> sample:reverse( [1,2,3] ).
[3,2,1]
3> sample:reverse( [{"aaa"}, 3.5, []] ).
[[],3.5,{"aaa"}]
4> sample:reverse( [] ).
[]

効率の悪さには目をつぶっていただくとして(※)、リストの並びを逆転する関数です。 「空リストか空でないリストを受け取って何かする関数」の書き方としては、 この、caseで[]か[Head|Tail]に分ける、という手があります。 [Head|Tail]の逆順とは要するにTailをひっくり返して後ろにHeadをくっつけたもの、 なので、上の関数のように書けるわけです。

(※)++演算はたぶんリストの長さに比例する時間がかかるので、 ++を使わず[X|Y]だけで書けるアルゴリズム、を考えると効率が上がります。 是非考えてみて下さい。^^;

例2

-module(sample).
-export([f/1]).

f(Rect) ->
	case Rect of
		{A, A} -> io:put_chars("正方形\n");
		{_, _} -> io:put_chars("長方形\n")
	end.

% _ はユニバーサルパターンなので、
% {_, _} と書いても左と右の要素が同じである必要はないことに注意
1> c(sample).
{ok,sample}
2> sample:f( {2,2} ).
正方形
ok
3> sample:f( {1,2} ).
長方形
ok

あんまりいい例思いつかなかったので申し訳ないのですが、 上の「tupleの左右が等しい」とか、あるいは 「片方が0」みたいな特殊な条件で分けるのにも使えます。 この例くらいだと if で十分ですが(^^;

関数定義 (2003/05/03)

関数定義について書くのはこれで三回目のような気もしますが、 今回は、パターンマッチの話が混ざります。

-module(sample).
-export([apply_all/2]).

apply_all(List, F) ->
	case List of
		[]          -> [];
		[Head|Tail] -> [F(Head) | apply_all(Tail,F)]
	end.
1> c(sample).
{ok,sample}
2> sample:apply_all( [1,2,3], fun (X)->X*X end ).
[1,4,9]

リストの各要素に関数を適用して結果のリストを返す、 という関数apply_allを書いてみました。 これは、次のようにも書けます。

-module(sample).
-export([apply_all/2]).

apply_all( [], F ) -> []; % ←注:セミコロン
apply_all( [Head|Tail], F ) -> [F(Head) | apply_all(Tail,F)].

引数に case-of を使って分岐、というのは非常によく使うパターンなので、 関数引数の部分に最初からパターンを書いておくことができます。 これもcase式と同じで、上に書いてある関数ほど優先して使われます。

head_of( [H|_] ) -> H.

tail_of( [_|T] ) -> T.

ユニバーサルパターンも当然使えるので、上のような定義も可能です。なお、 例えば上の例で 空リスト [] などマッチできない値が関数に渡されると、 長々としたエラーが発生します。

is_zero(0) -> true;
is_zero(_) -> false.

定数パターンもありなので、上のような一見不思議な定義もアリです。

judge( [] )      -> "empty list";
judge( [A|_] )   -> {"nonempty list with head", A};
judge( {A}   )   -> {"single element tuple", A};
judge( {A,B} )   -> {"double element tuple", A,B};
judge( {A,B,C} ) -> {"triple element tuple", A,B,C};
judge( _ )       -> "otherwise".

などなど。

無名関数の場合

無名関数の時も全くも同じで、 fun (...) -> ...; (...) -> ...; (...) -> ... end とセミコロン区切りで複数並べればOK。

1> F = (fun ({X,X})->"same"; ({X,Y})->"not same" end).
#Fun<erl_eval.5.123085357>
2> io:put_chars( F({1,2}) ).
not sameok
3> io:put_chars( F({1,1}) ).
sameok

落ち穂拾い (2003/05/03)

並行処理の話に入る前に、ここまでで書き残した機能を並べてみようかと。

guard

-module(sample).
-export([f/3]).

f(0, 0, 0) -> [0];
f(0, 0, C) -> [];
f(0, B, C) -> [-C/B];

f(A, B, C) when B*B-4*A*C > 0 ->
	Rd = math:sqrt(B*B-4*A*C),
	[(-B+Rd)/(2*A), (-B-Rd)/(2*A)];

f(A, B, C) when B*B-4*A*C == 0 ->
	[(-B)/(2*A)];

f(A, B, C) -> [].
1> c(sample).
{ok,sample}
2> sample:f( 0, 0, 0 ).
[]
3> sample:f( 1, 2, 1 ).
[-1.00000]
4> sample:f( 1, -6, 5 ).
[5.00000, 1.00000]
5> sample:f( 1, 1, 1 ).
[]

2次方程式の実解を計算する関数です。まず2字や1字の係数に 0 が来たときの場合分けはよしとして、注目すべきは when というキーワード。要するに、「パターン when 条件 ->」 と書いておくことで、データ構造の一致に加えて更に細かい条件で分ける マッチングが可能となります。case式でももちろん使えます。 「when~」の部分をguardと呼ぶらしい。

例外処理 catch/throw/exit

1> X=1/0, X.
=ERROR REPORT==== 3-May-2003::12:10:22 ===
Error in process <0.25.0> with exit value: {badarith,[
{erl_eval,eval_op,3},{erl_eval,exprs,4},{shell,eval_loop,2}]}
** exited: {badarith,[{erl_eval,eval_op,3},
                      {erl_eval,exprs,4},
                      {shell,eval_loop,2}]} **
2> X=(catch 1/0), X.
{'EXIT',{badarith,[{erl_eval,eval_op,3},
                   {erl_eval,expr,3},
                   {erl_eval,exprs,4},
                   {shell,eval_loop,2}]}}

上では、0除算エラー(badarith)のせいで実行プロセスが強制終了してます。 対して、下ではcatchでエラーになる式を修飾しておくことで、 エラー種別を変数Xに格納できています。

1> X=(catch 2/1), X.
2.00000

何もエラーが起きなければ素通し。

head( [H|_] ) -> H;
head( [] ) -> exit("head: empty_list").
1> catch sample:head( [1,2,3] ).
1
2> catch sample:head( [] ).
{'EXIT',"head: empty_list"}

0除算やマッチ失敗などは言語処理系がEXITを発行しますが、 exit 関数を呼ぶと自分でエラー処理もできます。 いわゆる例外処理として使うことができますね。

head( [H|_] ) -> H;
head( [] ) -> throw("head: empty_list").
1> catch sample:head( [1,2,3] ).
1
2> catch sample:head( [] ).
"head: empty_list"

exitの代わりにthrowを使うと、catchしたときに throwされたものが直接帰ってきます。 こっちの使いどころはイマイチわかってないのですが、 大域脱出などに使うと便利とのことです。

List comprehension

リストの内包表現。「1,2,3 のどれか1つと 1,2,3 のどれか一つ」 の2要素からなるtupleは全部で9通りあります。 これを全部格納したリストを書いてみましょう。

1> [{1,1},{1,2},{1,3},{2,1},{2,2},{2,3},{3,1},{3,2},{3,3}].
[{1,1},{1,2},{1,3},{2,1},{2,2},{2,3},{3,1},{3,2},{3,3}]

長いです。そこで、こんな書き方ができます。

1> [{X,Y} || X<-[1,2,3], Y<-[1,2,3]].
[{1,1},{1,2},{1,3},{2,1},{2,2},{2,3},{3,1},{3,2},{3,3}]

なんとなく、要素が等しい場合は除きたくなりました。

1> [{X,Y} || X<-[1,2,3], Y<-[1,2,3], X/=Y].
[{1,2},{1,3},{2,1},{2,3},{3,1},{3,2}]

「Xに[1,2,3]のどれか、Yに[1,2,3]のどれか、が入っているパターンのうち、 XとYが等しくないもの全て」のリストを作る、という作業になっています。 「変数名 <- 式」の形の部分を生成子(generator)と呼び、 条件式の部分をフィルタ(filter)と呼ぶそうです。

1> Xi = [1,2,3,4,5,6].
[1,2,3,4,5,6]
2> [X*Y || X<-Xi, Y<-Xi, (X+Y) rem 2 == 0].
[1,3,5,4,8,12,3,9,15,8,16,24,5,15,25,12,24,36]

サイコロを2つ振って、足して偶数になる場合の2つの目の掛け算結果のリスト。(^^;

メモ (2003/05/04)

電話回線がどうこうでなく一般人が使えるソフトとしては、 Wings 3D というモデラーが Erlang で書かれているそうです。ソースも公開されているので、 早速見てみようかと思います。

プロセス1 (2003/05/14)

Erlangでは、同時に動作する複数の"プロセス"を記述することができます。 プロセス間では、! でメッセージ送信、receive でのメッセージ受信によって通信を行います。

-module(test).
-export([hoger/1, send_and_receive/2]).

% 何か受信したらhogeを付けてプロセスPに送る無限ループ
hoger(P) ->
	receive X -> P ! {hoge,X} end,
	hoger(P).

% プロセスPにXを送信して、返事を待つ
send_and_receive( X, P ) ->
	P ! X,
	receive R -> R end.
1> c(test).
{ok,test}
2> Ho = spawn(test, hoger, [self()]).
<0.34.0>
3> test:send_and_receive(1, Ho).
{hoge,1}
4> test:send_and_receive([1,2,3], Ho).
{hoge,[1,2,3]}

順に解説。

普通に関数を起動するときは module:func( p1, p2, ... ). でしたが、別プロセスで関数を起動するときは、 spawn( module, func, [p1,p2,...] ) を使います。 このspawn関数の戻り値はプロセスIDとなっています。 ここでは、test:hoger 関数に自分のプロセスID( self() で取得できる ) を渡して起動しました。

hoger側では、メッセージが送られて来るのをreceiveで待ちます。 何かXを受け取ったら、返信先プロセス P へ {hoge,X} を送信。 また再帰的に自分を呼び出してreceiveに入ります。 ここではreceiveは単なる変数を取っていますが、case of と同様のパターンマッチが使えます。

さて一方、hogerを立ち上げた側では、何か新プロセスに送ってみます。 こちらでも送信は「プロセスID ! 送りたいもの」で受信は「receive - end」 と全く同じパターンですね。

プロセス2 (2003/05/14)

さっきの例では結局お互いreceiveしあっていて、 あまり並行実行している感じがしませんでした。別の例を書いてみます。 結構時間のかかる計算を複数個同時実行してみます。

-module(test).
-export([calc_fibs/1, fib_send/2]).

% [a,b,c,...] を受け取って fib(a), fib(b), ... を表示
calc_fibs( Lst ) ->
	calc_fibs_list( Lst ),
	receive_results( length(Lst) ).


% フィボナッチ数列{1,1,2,3,5,8,13,...}のバカ計算
fib(0) -> 1;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).
fib_send(P,N) -> P ! {N,fib(N)}. % 計算結果をプロセスPへ送る版


% リストの各要素からfibを計算するプロセス生成
calc_fibs_list([])      -> ok; % 何もしない
calc_fibs_list([N|Lst]) ->
	spawn( test, fib_send, [self(),N] ),
	calc_fibs_list(Lst).


% N個の計算結果を受け取る
receive_results( 0 ) -> complete; % 何もしない
receive_results( N ) ->
	receive
		{X, Fx} ->
			io:format("fib(~w) = ~w~n", [X, Fx] ),
			receive_results( N-1 )
	after 1000 ->
		timeout
	end.

calc_fibs( [a,b,c] ) が呼ばれると、fib(a)を計算するプロセスと fib(b)を計算するプロセスと fib(c)を計算するプロセスが生成し、 答えが3つ返ってくるのを待つ、という流れです。ただし、 1秒(1000ミリ秒)待っても答えが戻ってこなかったらタイムアウトするために、 receive~after を使っています。

io:format 関数は C の printf みたいなものです。

1> c(test).
{ok,test}
2> test:calc_fibs( [12, 3, 11, 4, 24, 31, 28, 10] ).
fib(12) = 233
fib(3) = 3
fib(11) = 144
fib(4) = 5
fib(10) = 89
fib(24) = 75025
fib(28) = 514229
fib(31) = 2178309
complete

実際に上のコードを実行してみると、 一個一個結果が戻ってくる様子が見えて面白いところです。 fib(10)の計算の方がfib(24)の16000倍くらいは早いので、 呼び出しの順番とは逆転して先に値が返ってきたりしてます。 この辺は環境依存ですが。

3> test:calc_fibs( [40,30,20,10] ).
fib(10) = 89
fib(20) = 10946
fib(30) = 1346269
timeout

fib(40)は遅すぎるので待ち時間1秒をオーバーしてしまいました。

おわりに (2003/05/15)

というわけで、この辺でこの記事は終わります。

並列性について主に調べようと思って始めたのですが、 なんか変数のbindの話でひっかかったり、 パターンマッチを熱く語ったりしてしまって予定とは変わった形になりました。 が、まあいいや。 上に書けなかったことで面白そうな内容を以下に箇条書きにしておきます。 興味を持たれた方は是非是非調べてみて下さい。

ではではー。

ノード

上のプロセスの例では、一つのerlangVMの中に複数のプロセスが走って、 お互いがやりとりしていました。実はそれだけでなく、 異なる erlangVM (node) で動いているプロセス間でも、 全く同じように!とreceiveでメッセージ通信ができるそうです。 応用法としては、複数マシンで分散計算とか。

ポート

他の言語で書かれたネイティブモジュールとのデータのやりとりや、 ファイルやストリームなどのI/Oは、低レベルな部分では「ポート」 を用いて実装されます。この「ポート」ともプロセスと同様に、 !で書いてreceiveで読むという形で通信が行われるそうな。

動的なモジュール

モジュールのロードやアンロードは実行中に動的に可能です。 要するに、プログラムを起動させたまま、 バージョンの古いモジュールだけを新しく入れ替えたりとかができます。

プリプロセッサ

C言語のプリプロセッサに似た、-define や -ifdef などの処理があります。 あまり興味がなかったので追ってないんですけど、結構強力っぽい。

ライブラリ

I/Oや数学関数、文字列処理などの標準的なライブラリの他に、 inets(HTTP/FTP)、ssl、gs(Graphics System)、odbc(SQL)、ic(IDL compiler)、 crypto、cos*(CORBA)、jinterface(communication with Java)、 parsetools(like yacc)、webtools(httpd) などなど…と色々と充実したライブラリ群が最初からついてきます。

presented by k.inaba   under NYSDL.