今までは、関数は副作用を持たないものとして扱ってきた。なので、時間の概念を気にする必要もなかった。一連の処理は、どのような順番で評価しても最終的に同じ結果が得られる。これを合流性(confluence property)とかチャーチ・ロッサー性と呼び、関数型プログラミングの理論的基盤となるラムダ計算の重要な帰結らしい。知らなかった。
ざっとWebを流し読みしたところ、ラムダ計算の体系では、ラムダ式をベータ簡約することが計算そのものとなるようだ。ベータ簡約は関数適用による正規化のこと。で、チャーチ・ロッサー性とか合流性とかは、ベータ簡約の順序がどうであっても最終的に同じ正規化されたラムダ式が得られる、ということを言っているようだ。これはつまり、どのラムダ式から評価・計算しても同じ計算結果が得られるということになる。…簡約、つまり計算が停止さえすれば。
つーか、これを調べてる間にラムダ計算の迷宮に迷い込んでしまった…大学で情報の基礎教育を受けていない身にはつらい…けど、勉強になる。
さて、これは不変の世界・参照透過性が保証された世界の話で、ここでの本題はScalaでの可変の世界の扱い方。ここでは、オブジェクトがhistoryによってその振る舞いを変える場合、オブジェクトは状態を持つという。このあたりは、純粋関数型とかそういうのはもう捨ててしまって、完全にオブジェクト指向の世界に入っている。いわば、Javaな人にとってはおなじみの世界。Javaとの違いといえば、変数は定義と同時に初期化しなければならない点くらいだろうか。なので途中の例は思い切って省略!つまんないし。
同値性と変更
「代入」の導入により、二つの式が同じかどうか判別するには、より正確な「同値性」の定義が必要になってくる。具体的には、可変の世界では以下のようにして同一性をテストするらしい。
- 式xと式yを含む任意の一連の処理Sを実行し、結果を観測する。
- Sの処理のうち、yをxにすべて置き換えた処理S’を実行し、結果を観測する。
- もしS’の実行結果がSの実行結果と異なっている場合、xとyは同じでない。
- SとS’の実行結果がすべて同じであれば、xとyは同じ。
これは、インスタンス自体が同じかどうかみたいな話をしているようだ。Javaのequalsに慣れた身としてはなんとももどかしい定義だ。理屈としてはこれで良いとして、実際上はどうするんだろう?
代入と置き換えモデルの崩壊
今までは、値の名前を常にその値を定義する式に置き換えることができた。例えば、
val x = new BankAccount; val y = x
のval y = xは、val y = new BankAccountと置き換えることができた。しかし、可変の世界ではこのモデルはもはや機能しない。このように書き換えてしまうと、違う意味になってしまう。
手続き的制御フロー
おなじみのwhile/do-whileがあります。なーんだ、あるじゃん!と手続きに馴染み深い自分は一安心してしまった。変数に値を代入できるから使える制御フローではあるよね、確かに。while (j <>
だけど結局、whileループも以下のような再帰の関数で置き換えることができる。
def whileLoop(condition: => Boolean)(command: => Unit) {
if (condition) {
command; whileLoop(condition)(command)
} else ()
}
こうすれば、可変オブジェクトは不要になる。なるほど。ちなみに、whileLoopは末尾再帰なので、実際はループとしてコンパイルされる。ということで、一定のスタック領域しか消費しない。
後は、回路をシミュレートするサンプルが延々続いてるけどこれは省略。おなじみだし。
まとめ
状態(代入)を導入すると、プログラムは複雑になる。とりわけ、参照透過性が失われる。が、状態はこれはこれでエレガントなプログラムを作るのに役立ったりもする。どちらを選ぶかは状況によりけり。
コメント