Erlang、単一代入と破壊的な変更

This entry was posted by on Friday, 18 May, 2007

昨日の文章を書いてから「ああ、べつに辞書に限った話じゃないからこれじゃ意味ねーな」と遅まきながら気付いたんですが、まあそれはしかたない。その点については
檜山さんから指摘をいただいてしまいました。おっしゃるとおりです。

問題点を抽象的な言葉で書くと、「Erlangは純粋関数型言語で、副作用がない」「しかも更新があった値を同じ変数に格納できない」ということですね。さて、どうするか。

解決策ですが、たとえばループを使うことができます。ループは Erlang では末尾再帰関数を使って書きますが、そうすると自分自身を呼び出すときに違う値を指定すれば、同じ変数に違う値を束縛できる。いわば、引数を状態変数として考え、自分自身を呼び出すのを更新とするわけです。

でも、世の中そんなにループばっかりじゃない。ここでやりたいのは普通の逐次的なプログラムを書くことでした。そこで「状態をもつループ」をほかのプロセスへと切り離してしまいます。メッセージを送るとその時点での状態を参照し、メッセージに従って値を返したり、更新したりできます。

では、コードを示します。

-module(dsets). -compile(export_all). new() -> spawn(?MODULE, loop, [gb_sets:empty()]). loop(Set) -> receive {From, {add_element, V}} -> From ! {self(), ok}, loop(gb_sets:add_element(V, Set)); {From, {del_element, V}} -> From ! {self(), ok}, loop(gb_sets:del_element(V, Set)); {From, {is_element, V}} -> From ! {self(), gb_sets:is_element(V, Set)}, loop(Set); {From, is_empty} -> From ! {self(), gb_sets:is_empty(Set)}, loop(Set); {From, size} -> From ! {self(), gb_sets:size(Set)}, loop(Set); {From, get_set} -> From ! {self(), Set}, loop(Set) end. rpc(Pid, Msg) -> Pid ! {self(), Msg}, receive {Pid, V} -> V end. add(Set, V) -> rpc(Set, {add_element, V}). del(Set, V) -> rpc(Set, {del_element, V}). elem(Set, V) -> rpc(Set, {is_element, V}). is_empty(Set) -> rpc(Set, is_empty). set_size(Set) -> rpc(Set, size). get_set(Set) -> rpc(Set, get_set).

シェルで実行させてみました。

Erlang (BEAM) emulator version 5.5.4 [source] [async-threads:0] [kernel-poll:false] Eshell V5.5.4 (abort with ^G) 1> c("/Users/mukai/erlang/dsets", [{outdir, "/Users/mukai/erlang/"}]). {ok,dsets} 2> Set = dsets:new(). <0.39.0> 3> dsets:is_empty(Set). true 4> dsets:add(Set, "jmuk"). ok 5> dsets:add(Set, "m-hiyama"). ok 6> dsets:elem(Set, "jmuk"). true 7> dsets:del(Set, "jmuk"). ok 8> dsets:elem(Set, "jmuk"). false

こんな具合でうまく行っています。

Erlang ではこういうモノを書くことはそれほど珍しいことではありません。 digraph がどうなっているか、ソースコードは見ていないのでわかりませんが、ごく簡単にはこんな風に実現できます、という例として読んでください。

さて、反論はいくつか思いつきます。第一に、非常にメンドくさい。いちいちこんなことを書いてられっか、という話があります。現実問題としては、こういうことをラップしてくれるライブラリがあると嬉しいんですが、あるんでしょうか。エラソーなことを書いていますがわたしも Erlang 初心者ですから、実はよくわかっていません。もっとも、これを実現する簡単かつラクな API ってちょっと思いつきませんけれど。

それからもうひとつ。このプログラムはガベコレの観点からの問題があります。ふつうの gb_sets は、参照するものがいなくなったら自動的に消滅することが期待できますが、これはプロセスなので簡単には止まりません。どうするか。

簡単な対処方法はあります。 Erlang のプロセスには「リンク」という機能があり、リンクで繋がったプロセス同士では、一方の終了をもう一方が検知したりできるようになっています。これを使い、接続してきたプロセスを覚えておいて、接続したプロセスがみんな終了したら自分も終了する、といった方法です(あとで参照しなくなる永続プロセスがいたらアウトだけど……)。

ところで、ほかの関数型言語では、どうなっているんでしょうか。たとえば Haskell では、同じように関数的な Data.Set というものがありますが、これはどうでしょうか。ただ、 Haskell や Lisp や ML の let 束縛では、過去に同じ名前の束縛があっても隠蔽してくれます。つまり、

import Data.Set as S ... let s = S.empty in let s = S.insert "jmuk" s in let s = S.insert "m-hiyama" s in ...

などと書ける、かというと実は Haskell は特殊事情があって書けませんが(笑)、 Lisp や ML なら書けます。でもね、こんな風に書くのはシロウトですよ。じゃあクロウトはどう書くんですか、というと、これはjijixiさんが書いてくれているので略させていただきます。

この fold は ugly に見えます。でもまあ、実際には処理が定型ならラップする手段があります(たとえば from_list のような関数がたいていは定義されています)。こんな風にやるのが「関数型言語風」なんです。

まとめ。

>
  • Erlang では副作用のある処理をプロセスを使って表現する(じつは io なんかも内部的には「入出力プロセス」へのメッセージとして表現されています) >
  • 関数型言語のパラダイムでは、コレクションタイプに逐次的にデータを入出力するようなことは違った風に書くため、単一代入でも実は問題になることはめったにない
  • Comments are closed.