previous up next_inactive
Up: Home

値と変化--Clojureにおける値のアイデンティティとステート

Rich Hickey

(翻訳 丸井淳史)

2009年12月22日(火)

The original document appears at http://clojure.org/state. This translation is based on November 28, 2009 version of the page.
数多くの間違いがあると思いますので、ご指摘ください。(Ref=参照、Agent=エージェントとしていますが、そのままRefとAgentとしたほうが良いような気もします。)

命令型プログラミング言語からClojureにやってくる多くの人は、Clojureのやりかたでは自分本来の力を発揮できないと感じる。また、関数型言語からやってきた人々は、Clojureの関数型プログラミングの領域から一歩でも外に出たら、Javaにあるようなステートを扱わないといけないと思っている。この文書では、命令型言語や関数型言語が直面する問題を、Clojureではどのように考えているかを紹介する。

1 命令型プログラミング

命令型プログラミングでは、コンピュータ内の世界(たとえばメモリ)を直接操作する。これは、現在ではあり得ない、単一スレッドであるという条件--あなたが命令を下しているあいだ「世界」は止まっている--を前提としている。つまり、ユーザーが「こうしろ」と言えばその通り行われるし、「あれを変えろ」と言えばその通り変化が起こる。命令型プログラミング言語においては、ユーザーからの命令とメモリの変化によって全てが行われるのである。

マルチスレッド以前においても、これはあまりいい考えではなかった。その上、並行処理をしようと思うと、問題は本格的になる。もはや「世界は止まる」という前提は崩れてしまう。それぞれが自らのことを全知全能だと考えている複数のオブジェクトがいて、皆が同時に行動している世界なのだ。「世界」が止まっているという幻想を排除し、お互いに悪影響を及ぼしあわないようにしなければならない。ミューテックス(相互排他制御)やロックを用いることで、それぞれがアクセスできる領域を制限することができるが、プログラミングは非常に複雑で、ときにバグが混入しやすくなり、メモリの操作を共有するためにかかるオーバーヘッドも見過ごせないものとなる。つまり、うまく行かないのだ。

2 関数型プログラミング

関数型プログラミングでは、より数学的な視点で物事を考える。プログラムは、入力として何かしらの値をとり、他の値を作り出す、関数である。命令型プログラミングとは異なり、関数型プログラムは関数の外部に影響を及ぼすこと(副作用)を避けるため、理解しやすく、筋道を立てやすく、テストしやすい。純粋な関数型プログラミング言語であるかぎり、関数が「世界」に影響を与えることはないので、並行処理は問題なく行える。

3 動作モデルと同一性

巨大な関数に過ぎないプログラム--たとえばコンパイラや自動定理証明システム--もいくつかあるものの、その他のプログラムはたいてい違う。その他のプログラムはどちらかというと実際に動く模型のようなもので、それらは私がアイデンティティと呼ぶものをサポートしていなければならない。アイデンティティというのは、時間によって変化する値と結びついた、安定した理論的な実体である。「世界」を表現するために、人間がアイデンティティを必要とするように、モデルにもアイデンティティが必要である。アイデンティティが失われ、「今日」や「アメリカ」といったものが、ひとつの変化しないものを指すとしたら、どうなってしまうことか。さて、ここで言っているアイデンティティとは名前のことではないことに注意してもらいたい (私は自分の母を「おふくろ」と呼ぶが、読者は私の母をそうは呼ばないだろう)。

すなわちこの議論においては、アイデンティティとは、ステート--そのときどきの値--を持つ実体である。そして、値は変化しないものである。42は変化しない。2008年6月29日は変化しない。どんな邪悪なクラス・ライブラリが誤解させようとしても、点は移動せず、日付は変化しない。集合体も値である。私が好きな食べ物の集合は変化しない。もし将来、私が他の食べ物を好きになったとしたら、それは別の集合である。

アイデンティティとは、私たちが、変化し続け機能的に新たな値を作り続ける現実世界の連続性を考えるための、思考の道具なのである。

4 オブジェクト指向プログラミング

オブジェクト指向は、プログラムにアイデンティティとステートのモデルを提供する仕組みのひとつである (その他のものとしては、ステートと動作の結合や階層的分類などがあるが、ここでは扱わない)。典型的なオブジェクト指向ではアイデンティティとステートを統合している。すなわちオブジェクト (アイデンティティ) は、状態を表す値の保存されているメモリへのポインタである。ステートを複製する以外に、アイデンティティと独立してステートを得る手段はない。他のオブジェクトからの操作をブロックしなければ、現在のステートの情報を得ることも複製することもできない。あるアイデンティティのステートを他の値に変更することも、メモリ内の情報を直接変更することでしか実現できない。つまり、典型的なオブジェクト指向には命令型プログラミングが染みついているのだ。必ずしもこうでなくてもいいはずだが、普通のオブジェクト指向言語 (Java/C++/Python/Rubyなど) はこうなっている。

オブジェクト指向の考え方に慣れた人は、プログラムとはオブジェクトの値を変化するものだと考える。例えば42のような値の真の姿は変化するものではないと正しく理解しているが、その考え方を延長してオブジェクトも変化しないものだと考えたりはしない。これは彼らが使う言語のせいだ。そういった言語では、アイデンティティやオブジェクトと同様に値を扱うためデフォルトで書き換え可能であり、そのため特に卓越したプログラマーでない限り、アイデンティティを作りすぎたり、値にするのが適切なものをアイデンティティにしてしまうのだ。

5 Clojureプログラミング

他の方法もある。それは、アイデンティティとステートを分離し、プログラミングをより効率的にする方法だ。ステートを「メモリブロックの中身」ではなく「あるアイデンティティと現在結びついている」と理解しなければならない。そうすることでアイデンティティは様々なステートと結びつくことができ、ステートそのものは変化しない。アイデンティティはステートではなく、アイデンティティがステートを--どんな時でもどれかひとつのステートを--保持するのだ。そして、ステートは変化することのない真の値である。もしアイデンティティが変化しているように見えるのなら、それは次々と異なるステートと結びついているからである。これが、Clojureでの考え方だ。

Clojureモデルでは、値の計算は純粋に関数型である。値は変化しない。新しい値は、古い値をもとに計算された関数からの出力値であって、値そのものが変化したのではない。しかし論理的アイデンティティは、値へのアトミックな参照 (Refs) とエージェント (Agents) によってサポートされている。参照の変化はシステムによって制御/管理されている--協調は任意でも手動でもない。各スレッドの協調的な働きによって「世界」は前に進み続け、プログラミング言語/システムであるClojureは「世界」の一貫性を保つ。参照先の値 (アイデンティティが指すステート) は管理なしに読むことができ、複数スレッド間で共有することもできる。

スレッドをたとえひとつしか使わなくても、このようなプログラムの作り方には価値がある。アイデンティティと値の結びつきから関数値の計算が独立していれば、プログラムは理解しやすくテストしやすいものになる。そして、どうしても必要なときには、スレッドを追加するのも簡単になる。

5.1 並行処理

並行処理を用いるということは、全知全能の幻想を失うということだ。プログラムは他にどんな参加者がいるか、「世界」が常に動き続けているということ、などを知っていなければならない。あるアイデンティティに結びついたステートを参照するとき、得られた値はある瞬間のスナップショットであり、次の瞬間には他のステートになっている可能性があるのだ。しかし、決断をしたり報告をするときには、それで十分なことが多い。我々人間というものは感覚器官から得られたスナップショットを扱うことには長けているようだ。得たステート値はイミュータブルなので、処理中に変化することはないということは利点だろう。

反対に、ステートを新しい値に変化させるには、現在の値とアイデンティティへのアクセスが必要になる。Clojureの参照とエージェントはこれを自動的に処理してくれる。参照では、あらゆる操作はひとつのトランザクションの中で行わなければならない (さもないとClojureは例外を発生する)。「世界」のある瞬間をフリーズさせたかのように見せ、他のスレッドが操作をしている最中は、新たな変更作業は進められない。トランザクションは複数の参照への同時変更をサポートしている。一方、エージェントではひとつのものへの同時処理が可能だ。関数と引数を投げておけば、未来のどこかの時点で、関数がエージェントの現在のステートに渡され、関数の戻り値がエージェントの新しいステートになる。

どちらの場合においても値は変化せず、共有もできるので、プログラムにとって「世界」は静止した状態として扱うことができる。ただ、「値は変化しない」ということは、古い値から新しい値を作り出すことが効率的にできなければならないことを意味する。Clojureでは永続的データ構造のおかげで、それを実現できている。しばしば良いことだと言われているイミュータビリティを、永続的データ構造のおかげでようやく手にすることができる。アイデンティティのステートを更新するには、まずステートの現在の値を読み、それをもとに純粋な (副作用のない) 関数を使って新しい値を作り、その値を新しいステートに保存するという手順を踏む。この手順はaltercommutesendといった関数によって簡単かつアトミックになっている。

6 メッセージパッシングとアクター

アイデンティティとステートを実現する他の方法もある。その中でも有名なのはErlangでも採用されているアクターモデルだろう。アクターモデルでは、ステートはそれぞれのアクター (アイデンティティ) の中に組み込まれており、アクターにメッセージ (値) を送ることでしか中身を見たり変更したりすることができない。Erlangのような非同期型システムにおいてアクターの状態を得るためには、アクターにメッセージを送り、応答を待ち、アクターが応答を返すという流れになる。ここで、アクターモデルは分散プログラムにおいて生じる問題を解決するために考案されたものであることを知っておくことが重要になる。分散プログラムでは、複数の「世界」(アドレス空間) があり、直接メモリを覗くことはできず、情報のやりとりは信頼性が完全ではない信号チャンネルを用いて行わなければならない、などのさらに難しい問題に直面する。アクターモデルでは透過分散がサポートされている。この方法で全てのコードを書けば、他のアクターがどこに存在していても構わなくなり、コードを変更することなしにシステムを複数のプロセスやマシンに分散させることができる。

Clojureでは以下のような理由で、Erlangスタイルのアクターモデルを使用しないことにした。

分散処理がどうしても必要になったときには、Clojureでも分散プログラミングのためのアクターモデルを採用するかもしれないが、単一プロセス・プログラミングではかなり面倒なことになるだろう。もちろんこれは個人的な見解だが。

7 まとめ

Clojureは、プログラムを明示的にモデルとして扱い、アイデンティティとステートを堅牢かつ使いやすく扱うことができる、単一プロセスで並行処理が可能な関数型言語である。

オブジェクト指向言語からClojureにやってきた人々は、まずmapなどの永続コレクション型をオブジェクトの代わりに使うことをおすすめする。可能な限り値を使い、オブジェクトがアイデンティティをモデル化していると確信できるときには--じっくり考えてみてもらいたいが、思ったよりも少ない機会になると思う--mapをステートとして参照やエージェントなどを用いることで、変化するステートを持つアイデンティティを実現できる。値の詳細を隠蔽したり抽象化したければ、値の読み書きをするための関数を書けばよい。ポリモーフィズム (多態性) が欲しければ、Clojureのマルチメソッドがある。

Clojureにはミュータブル (変更可能) なローカル変数がないので、ローカル変数を使ったループは難しい。代わりにrecurreduceを用いて関数型言語っぽくやってはどうだろうか。



MARUI Atsushi
2013-01-12