2.5 からの新要素。 今まで yield は単なる構文だったから値を返さなかったが、今度から式になって値を返すようになったという話。 具体的には next() メソッドが呼ばれたときは (for 文なんかでもこれが呼ばれる) None を返し、send(arg) メソッドが呼ばれたときは arg を返す。 それで何が嬉しいのかイマイチ思い付かないんだけど、とりあえず fold (Python でいう reduce) が作れそうだと思ったんで試してみた。 や、もちろん、ジェネレータなんぞ使わなくても作れるんだけど……
In [67]: !cat test.py
def fold(f, lst, init=None):
if init is None:
init = lst[0]
lst = lst[1:len(lst)]
def gen():
i = 0
r = init
yield r
while i < len(lst):
r = (yield f(r, lst[i]))
i += 1
yield r
g = gen()
try:
r = g.next()
while True:
r = g.send(r)
except StopIteration:
return r
else:
raise Exception
In [68]: execfile('test.py')
In [69]: import operator as op
In [70]: fold(op.add, range(1,5))
Out[70]: 10
In [71]: fold(op.add, range(1,5), 2)
Out[71]: 12
In [72]: fold(op.add, range(1,5), 0)
Out[72]: 10
In [73]: fold(lambda a,b: a + [b], range(1,5), [])
Out[73]: [1, 2, 3, 4]
この仕様に合わせて for 文を拡張したりしないと、ちょっと使いづらい感じはする。 send() を呼んだ時点で次の yield まで行っちゃうのが良くない気が。 値を投入するだけにしとけば良かったのに。
他にも throw(type) でジェネレータ内部に type 型の例外を起こさせたり、close() で GeneratorExit 例外を起こさせたりできるらしい。 こいつらは、今のままの for 文でも使い道があるかもしれない。 単純なところだと…
In [74]: def gen(): ....: try: ....: for i in range(10): ....: yield i ....: except: ....: pass ....: In [76]: g = gen() In [78]: for i in g: ....: if i < 4: ....: print i ....: else: ....: g.close() ....: 0 1 2 3
こんなとか。 まあ、この例なら break で十分だが(苦笑)。 throw() を使えば、巻き戻し可能なジェネレータとか作れるんじゃないかな。 そういうのが必要なシチュエーションて、どんだけあるのか知らんけど。
前にも似たようなこと書いたことある気がするけど。 わりとネタっぽいけど、それなりに真実を含みつつある感じでもある。 では、以下。
型安全でプログラムの正当性を保証することはできないけど、型安全を保証することで頭のよくない人間の間違いを検知することは可能なのです。 型の不一致でコンパイルエラーが出るなら、それは書いてる人間が間違ってるのです。アホなのです。 どんな言語だって、今書いてる場所でどんなデータを扱ってるかを意識しないで書くことは不可能でしょう。 OCaml なら静的型付けで関数のオーバーロードも無い (整数と浮動小数点数がそれぞれ別の演算子を使う病的さ) ので、自分が今どの型を扱っているつもりなのか一目瞭然なのです。 そして、そのつもりが間違っていればコンパイル時にあらかじめわかるのです。
型推論のおかげで、よほどややこしい状況でない限り、型情報なんかはまったく書かずにコードを書き進められるにもかかわらず、その病的なまでの静的さでコードを読めば何の型を扱おうとしてるかわかるわけです。 「Lisp サイコー」とか臆面もなく言えちゃうような人は、スーパーハッカーか、もしくはよほどの身の程知らずじゃないかと思いますが、そうじゃない凡人は自分が今扱ってる値がほんとに思ったとおりの型なのか完璧に把握なんてできないのです。 そして間違って扱ってた場合に、実行してみるまで間違ってるかどうかわかんないなんて怖すぎるのです。 わしは Ruby で何か書くときも、いつもぶるぶる震えてるのです。
だからみんな TDD, TDD って言うんでしょ。 あれって結局、あらかじめ型の指定をして、それが正しいかしょっちゅう確かめましょうってことじゃないですか。 OCaml ならそんなテスト書かんでも良いのですよ。
……まあ、具体的なふるまいまでは感知できないから、それは結局テスト書かなきゃだめですがね。
暗黙のキャストが無いのも安心の理由の一つ。 関数オーバーロードも一種の暗黙のキャストじゃないかと思うんですが、そんなのも無いから心配ありません。 少なくとも、自分が何の型を扱ってるのかわかんなくする罠としては、両方似たようなもんです。 整数を扱ってるつもりなら、そこは絶対整数を扱おうとしてるのです (もちろん凡人は間違うものなので、実際にはそこは整数じゃないかも知れませんが)。 知らないうちに小数とかになってたりしちゃ困ります。 ハスなんとかって言語がはやったりしてますが、あれはだめです。
xxxx> 1 / 2 0.5 xxxx> (1 / 2) == 0 False (一部伏せ字)
は? なんですかこれは。 1 / 2 は 0 でしょ。 何言ってるんでしょうね、まったく。 OCaml ならその辺ばっちりですよ。
# 1 / 2;; - : int = 0 # 1. /. 2.;; - : float = 0.5
この安心感がたまりませんね。 OCaml ほど安心感のある言語はなかなか無いですよ。
なんかおもしろいもん無いかなーと port search でてけとーに探してたら Erlang があったんで、思わず入れてみたりしたのであった。 Fink には無かったし、FreeBSD に ports で入れようと思ったらなぜかビルドに失敗してそのままだったりしたのよね。 そんなわけで、k.inaba さんの残した足跡とか踏ませてもらいつつ遊ぶ。
基本的なところは ML とか Haskell とか触ったことあれば、迷うところはあんまり無さそう。 変数名が大文字かアンダースコアで始まらないといけないってのが、ちょっと変わってる感じ。 小文字から始まる識別子は atom と呼ばれていて、どうもシンボルみたいなものっぽい。
% erl Erlang (BEAM) emulator version 5.5.2 [source] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.5.2 (abort with ^G) 1> is_atom(hoge). true 2> is_atom(Hoge). ** 1: variable 'Hoge' is unbound **
モジュールや関数は atom に束縛されるみたいだけど、対話環境では関数定義ができないみたいで Hugs の悪夢が蘇える。 クロージャがあるんで変数にクロージャを束縛すれば良いんだけど、どうも再帰ができないみたいで、結局エディタで関数定義しないと遊びきれない予感。
3> F = fun (X) -> if X > 0 -> F(X-1); true -> X end end. ** 1: variable 'F' is unbound **
ともあれ、Erlang と言えば平行プログラミングだよなーってことで、とりあえずずっと前のお遊びコードを移植してみた。 なかなかライブラリのリファレンスが見つけられなくて (さっきようやく見つけた) どうでも良いものまで自前で書いてたりするけど、まあそこら辺は関数定義の練習になったから良し。 ほんとは結果を receive するプロセスを別に作ってやった方が平行っぷりが出て楽しいと思うんだけど、そのプロセスをキレイに止める方法がわかんないんでやめた。 なんかパッと見 Haskell のコードみたいに見えるけど、引数の数が同じ (でパターンが違う) 関数は ; で繋げてやらないといけないので、Haskell みたいな微妙な間違いはしなさそう。
% cat prime.erl
-module(prime).
-export([isPrime/1,
proc/2,
reverse/1,
range/1,
range/2,
range/3,
deleteItem/2,
primeList/1
]).
isPrime(Num,C) ->
if C * C > Num -> Num;
true ->
if Num rem C == 0 -> false;
true -> isPrime(Num,C+1)
end
end.
isPrime(0) -> false;
isPrime(1) -> false;
isPrime(Num) -> isPrime(Num,2).
proc(Pid, Num) ->
Result = isPrime(Num),
Pid ! {self(), Result}.
reverse([], Result) -> Result;
reverse([X|XS], Result) -> reverse(XS, [X|Result]).
reverse(List) -> reverse(List, []).
range(Start,Stop,Step,Result) ->
if Start < Stop -> range(Start + Step, Stop, Step, [Start|Result]);
true -> Result
end.
range(Start, Stop, Step) -> range(Start, Stop, Step, []).
range(Start, Stop) -> range(Start, Stop, 1).
range(Stop) -> range(0, Stop, 1).
deleteItem(_, [], Result) -> Result;
deleteItem(Item, [X|XS], Result) ->
NewResult = if X == Item -> Result;
true -> [X|Result]
end,
deleteItem(Item, XS, NewResult).
deleteItem(_, []) -> [];
deleteItem(Item, List) when is_list(List) -> deleteItem(Item, List, []).
primeList([], [], Result) -> Result;
primeList([], PIDs, Result) ->
receive
{Pid, false} ->
primeList([], deleteItem(Pid, PIDs), Result);
{Pid, Num} ->
primeList([], deleteItem(Pid, PIDs), [Num|Result])
end;
primeList([X|XS], PIDs, Result) ->
Pid = spawn(prime, proc, [self(), X]),
primeList(XS, [Pid|PIDs], Result).
primeList(Max) ->
List = range(Max),
case List of
[] -> [];
[_|_] -> primeList(List, [], [])
end.
4> c(prime).
{ok,prime}
5> prime:primeList(100).
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]
ソートしてるわけでもないのにキレイに順番に並んでて、ちっとも平行動作してる意味無いのバレバレ(笑
複数のノード (VM) で動いたりしてこそ Erlang ですよ (ほんとか?)。 ってことで簡単なテストとか。 まあ、簡単と言いつつ、こんだけやるのに一時間くらいかかったのは内緒だってば。
まずソースを用意。 メッセージを待って、飛んできたメッセージを表示する関数 wait() が定義してある。 間違った形式でメッセージが来た場合は error と表示してプロセス終了。
% cat hoge.erl
-module(hoge).
-export([wait/0]).
wait() ->
receive
{ Node, Msg } ->
io:fwrite("~s : ~s~n", [Node, Msg]),
wait();
_ -> io:fwrite('error~n')
end.
で、これをまず一つ目の VM で読み込む。
% erl -sname hoge
(hoge@monolith-air)1> c(hoge).
{ok,hoge}
この場合、'hoge@monolith-air' というのがこの VM のノード名。 そして、別の端末でもう一つ VM を立ち上げる。
% erl -sname fuga (fuga@monolith-air)1> Node = 'hoge@monolith-air'. 'hoge@monolith-air' (fuga@monolith-air)2> net_adm:ping(Node). pong
net_adm:ping 関数は Node が生きてる場合 pong を返す (死んでるときは pang)。 こちらでは hoge モジュールは読み込まないので、
(fuga@monolith-air)3> hoge:wait().
=ERROR REPORT==== 5-Jan-2007::22:23:19 ===
Error in process <0.35.0> on node 'fuga@monolith-air' with exit value:
{undef,[{hoge,wait,[]},{erl_eval,do_apply,5},{shell,exprs,6},{shell,eval_loop,3}]}
** exited: {undef,[{hoge,wait,[]},
{erl_eval,do_apply,5},
{shell,exprs,6},
{shell,eval_loop,3}]} **
hoge:wait() を呼ぼうとしても、このようにエラーになる。 では、ノード hoge 上に fuga からプロセスを作成しよう。
(fuga@monolith-air)5> Pid = spawn(Node, hoge, wait, []). <4833.45.0>
これで、hoge 上でプロセスが動いてる状態になったはず。 確かめ方がまだよくわからないので、とりあえず "はず" ってことで。 次にこのプロセスにメッセージを送る。
(fuga@monolith-air)7> Pid ! {node(), 'Hello, World!'}.
{'fuga@monolith-air','Hello, World!'}
fuga@monolith-air : Hello, World!
2 行目は入力した式の返り値で、3 行目がプロセスが表示した結果。 多分、Term の指定とかをすれば向こう側の端末に表示したりもできるんじゃないかと思うけど、まだちょっとそこまで手が回らない。 適当に間違った形式のメッセージを送ってやれば、プロセスは終了する。
(fuga@monolith-air)9> Pid ! 'bye'. bye error
基本的には IP ベースの仕組みらしいので、別のマシン同士でもネットワークの設定さえちゃんとしてやれば同じようにできるはず。 すんごく簡単。 やばいなー、おもしろいなー、Erlang 。 なんかこう…いろいろやってみたくなる誘惑がふつふつと……
さっきの素数を計算するアレ、OCaml で書いたのだと…
% time ./prime 10000
Fatal error: exception Sys_error("Thread.create: Resource temporarily unavailable")
./prime 10000 1.71s user 4.87s system 85% cpu 7.704 total
こんな感じなわけさ。 んで、さっきの prime.erl に、
main([Arg|_]) ->
Max = list_to_integer(atom_to_list(Arg)),
List = primeList(Max),
io:fwrite('~s~n', [integer_to_list(lists:max(List))]).
こんな感じの定義を足して (export リストに main/1 ってのも追加して)、
% erl -compile prime
% time erl -noshell -s prime main 10000 -s init stop 9973 erl -noshell -s prime main 10000 -s init stop 5.94s user 0.34s system 79% cpu 7.861 total
余裕。さらに、
% time erl -noshell -s prime main 100000 -s init stop 99991 erl -noshell -s prime main 100000 -s init stop 612.76s user 20.79s system 90% cpu 11:41.34 total
かなり時間かかったけど、ちゃんと終わる。偉い。 だってこの実装だと、100,000 個のプロセス (他の言語で言うところのスレッド) をががーっと作ってるわけよね。 なんかもう無茶苦茶だな、この言語。良い意味で(笑
ちなみにアクティビティモニタで眺めてたところでは、メモリ使用量のピークは 13MB くらいだった。 んで、そっからじわじわと (たぶんプロセスがだんだん終了していって) 下がってくるのがおもしろかった。 なんだろね、GC が逐一回収してるんだろか。 GC の方式が何なのかわかんないけど、erl -man erlang で組み込み関数を見てみると、プロセスごとに GC を動かす仕組みがあるみたいだから、プロセスが終了するときにその都度動いてるのかも。