Erlang の得意・不得意

追記
西尾さん から「Lisp や Haskel も Erlang とおなじ『頭から読んでいく以外は遅いリスト』だと思うよ」というツッコミをいただいた。感謝。
むむむ。ということは Lisper や Heskeler には既知の事実であったか。ということで以下の記事は「Erlang には Ruby/Python/Perl の言う『配列』はないよ!」と読んでいただければ嬉しい。
やっぱり Lisp は紳士のたしなみ(?)として押さえておくべきかなあ。
追記おわり

404 Blog Not Found:そろそろerlangについて一言いっとくか
http://blog.livedoor.jp/dankogai/archives/50832431.html

弾さんも書かれているとおり、Erlangシンタックスは根本的にミスを誘発しやすいものになっていて、まあとにかく褒めてあげたいところはまるでない。
例えばセンテンスの区切りは "." "," ";" "end" (無し) の5種類があって、間違うと動かないが、慣れないと間違っていることがわからないし、慣れても最初に書き下したときはたいてい間違っているという、とてもストレスのたまりやすいつくりになっている。


試しに Erlang クイズ! [A]〜[E] の ? の位置には "." "," ";" (無し) のどれかが入ります。さあどれが入るでしょう?

loop() ->
    receive
        {ok, X} ->
            io:format("~p~n",X)?      - [A]
            loop()?                   - [B]
        {ng, X, Y} ->
            bar(X, Y)?                - [C]
            loop()?                   - [D]
        after 5000 ->
            io:put_chars("loop_out")? - [E]
    end.

ちなみに間違えた場合は "illegal guard expression" などの素敵なエラーをプレゼント(ガード条件式なんかちっとも間違ってないのに……)。


ただ、

exportってexportしてねえじゃん
単にpublicにしているだけで。おかげで

reverse (1..32) # perl, et al.
[1..32].reverse # ruby, et al.

lists:reverse(lists:seq(1, 32))

ですよ!

には一応 import という技があって、

-import(lists, [reverse/1, seq/2]).

としておけば reverse(seq(1, 32)) と書ける。python の from 〜 import みたいなもん?
まあ「関数型」なのでシンプルな記述を求めるのはこのへんで勘弁してあげたいところ。


実は Erlang はそれら以外にも根本的に苦手としているものがある。
Erlang にて配列に相当するものは「リスト」と呼ばれているのだが、これは一般的な処理系で言う「配列」とはかなり異なる性質のもので、同じ感覚で使うと手痛い目に遭う(遭った)。


論より証拠。

-module(test2).
-export([m/0, m/1]).
-export([array/1, array2/1, process/1, process_child/2, queue/1, ets/1, dets/1]).

-define(COUNT, 30000).

m()->
	m(list),
	m(list2),
	m(queue),
	m(ets),
	m(dets),
	m(process).
m(Func)->
	L = timer([], 10, ?MODULE, Func, [?COUNT]),
	io:format("~p=~100p\n  average=~f~n", [Func, L, lists:sum(L)/length(L)]).
timer(L,0,_,_,_) -> L;
timer(L,C,M,F,A) ->
	timer([element(1,timer:tc(M,F,A))|L], C-1, M, F, A).

list(C)->
	list_postadd([], C),
	finish.
list_postadd(L, 0)->L;
list_postadd(L, C)->
	list_postadd(L++[C], C-1).

list2(C)->
	list_preadd([], C),
	finish.
list_preadd(L, 0)->L;
list_preadd(L, C)->
	list_preadd([C|L], C-1).

queue(C)->
	queue_add(queue:new(), C),
	finish.
queue_add(Q, 0)->Q;
queue_add(Q, C)->
	%queue_add(queue:in(C, Q), C-1).
	queue_add(queue:cons(C, Q), C-1).

ets(C)->
	ets_add(ets:new(etstest, [duplicate_bag]), C),
	finish.
ets_add(T, 0)->T;
ets_add(T, C)->
	ets:insert(T, {C}),
	ets_add(T, C-1).

dets(C)->
	dets:open_file(detstest, [{file, "test.dets"}]),
	dets_add(detstest, C),
	dets:close(detstest),
	finish.
dets_add(T, 0)->T;
dets_add(T, C)->
	dets:insert(T, {C}),
	dets_add(T, C-1).

process(C)->
	spawn_link(?MODULE, process_child, [self(), C]),
	receive
		X->X
	end.
process_child(Parent,0)->
	Parent ! finish;
process_child(Parent,C)->
	spawn_link(?MODULE, process_child, [self(), C-1]),
	receive
		X ->Parent ! X
	end.

これはリスト、queue(FIFO, FILO 両対応なキュー)、ets(メモリDB。DBMっぽいやつ)、dets(etsのファイル版) に3万個の要素を追加し、その実行時間(マイクロ秒)を10回計測&平均を出力するプログラム。
リストは要素の追加の仕方を変えて2種類用意( list=リストの後ろに追加、list2=リストの前に追加 )。
また、「3万個のプロセスを起こしていき、全部起きたら、全プロセスにわたって順にメッセージをリレーしながらプロセスを終了させていく」という処理もおまけに入っている心憎い仕様。


その実行結果。

list=[4717407,4709192,4767290,4693415,4707939,4750385,4706151,4737291,4664512,4731640]
  average=4718522.200000
list2=[1368,1365,1373,1344,1365,1402,1355,1387,1383,1524]
  average=1386.600000
queue=[4551,5000,4486,4464,4865,4702,4703,4702,4421,5594]
  average=4748.800000
ets=[21201,21240,21524,21282,22840,21458,21441,21211,18336,15823]
  average=20635.600000
dets=[632953,635546,630081,626476,633329,632602,618765,625617,619436,657895]
  average=631270.000000
process=[163617,164473,163991,165518,164270,164068,164693,166663,165595,327039]
  average=180992.700000
ok

list2 (リストの前に追加)はまあ期待通り一番成績がいいのだが、list (リストの後ろに追加) がもう強烈ダントツ遅いことがわかる。ファイル書き出しのある dets と比べてさえ優に8倍ほど遅い。


実は Erlang の list は「先頭への追加・先頭からの値の取り出し・2番目以降の部分配列の取り出し」以外の操作はものすごくコストが高い特殊な構造になっている。
つまり、配列と言うより First In Last Out のキュー、「非常用に配列っぽい操作も若干用意されているスタック」だと思っておいた方がいい。
そう考えると、関数型といえば map や filter が得意というイメージがあるんだけど Erlang のは扱いにくいよなあとか、「リストのn番目に値を設定する」関数が lists モジュールにすら存在していないとかいうのも納得?
ともあれ、まともになにがしかのロジックを実装しようと思ったら、配列(的概念)はまあまず必要になると断言していいかと思うが、「配列があるように見せかけて、実は無い!(どどーん)」というのはなかなかサプライズな落とし穴なんじゃないかと。


一方、process は3万プロセスもハンドリングしたとは思えない、前評判通りの数字(単位は「マイクロ秒」ですんで、念のため)。
この Erlang のプロセスは単に軽量というだけでなく、プロセス間で何も共有しないため、何も変更せずに1台のマシン内の複数コアに対応させたり、物理的に別のマシンで動かしたりできる。例えば他のマシン上の Erlang ノードに送り込んで実行させたい場合も、spawn の第一引数にノードIDを追加するだけ。
そして、何も共有しない分、プロセッサの数だけリニアに性能が上がっていくことが期待できる。これが Joe Armstrong さんの「あなたが書いたErlangのプログラムはNコアのプロセッサならN倍速く動くはずだ」という根拠。
でも、各プロセスが共通の大きなデータセットをハンドリングする必要がある場合は Erlang のメリットを活かせない可能性が高そう……


ということで Erlang の得意・不得意は、


・1処理単位に必要なデータが少ない場合は得意
・同じような処理が同時多数発生する場合は得意
・大きなデータ(特に配列)を取り扱うのは不得意
・一般的なプログラマのご機嫌をとるのは不得意w(ちょっと変な人ならぶーぶー文句言いながら使ってくれるけどw)


結論: Erlang はネタと人を選ぶ。