猫型プログラミング言語史観(2) 〜あるいは実行コンテキストにどう立ち向かうのか〜

前回のおさらいと今回の話

前回は手続き型パラダイムオブジェクト指向パラダイムを見比べたときに、「ひとかたまりのデータとそれを操作する手続きを一カ所にまとめて守る」という方向に言語が進化していったというひとつの史観を示しました。その中で返答として「構造化プログラミング」の時点でその視点はすでにあるという指摘を頂いたりもしました。ただ、「ひとかたまりのデータとそれを操作する手続きを一カ所にまとめる」という発想もオブジェクト指向の「ひとつの側面としては」たしかにありますし、その側面を見ると、オブジェクト指向言語に「言語デザインでもってプログラマーがそれを行いやすくした」という面を見いだすことができそうです。そして、その視点に立ったときに「臭ってくる」ヤバい設計として、「データが露出してる」「別のクラスのデータいじってる」「複数の異なるデータに関心をもってしまっている」というものを挙げてみました。

さて、今回はそこから一歩進み、純粋関数型言語の話です。

オブジェクト指向はデータを守りきれたのか

さて、「関連の深いデータや手続きをまとめる」「データを外からまもる」という視点でオブジェクト指向を見たときに、前回までのオブジェクト指向は本当にデータを守ることに成功しているのでしょうか……?たしかに、private な属性を定義すれば、その値はそのクラスの外からは変更されないことが保証されます。しかし、ほんとうにそれで「十分」なのでしょうか……?

わたしの考えでは、答えは NO です。たしかにモジュールの外からデータを守ることには成功したオブジェクト指向ですが、「実行コンテキスト」からはデータを守りきれませんでした。ここで「実行コンテキストからデータを守ることができない」とは少なくともふたつの意味を指しています。ひとつは、複数の実行コンテキストから守れないということ。もうひとつは、以前の実行コンテキストからデータが守れないということ。

たとえばスレッドセーフの話

まずは前者、「複数の実行コンテキストからデータを守れない」について見ていきましょう。例として、スレッドが挙げられます。クラスの中にデータを隠蔽して、「クラスの外から操作をされるってことがないぜ」ということを保証できたとしても、それだけでは「複数のスレッドが(ほぼ)同時に値を書き換えちゃって変な挙動をする」みたいなことは防げません。たとえば Java でプログラムを書くとき、プログラマーは常に「もしかしたらこの値は別のスレッドが書き換えてしまっているかもしれない」とおびえながらプログラムを書くことになります。同時に書き込みをすることだけが問題ではありません、同時に2つの実行コンテキストが走っていて、片方が書き込み、片方が読み込みであっても、「ほんとは読まれる前に書き込みしておかないとだめなんだけど、書き込まれる前に読んじゃった」とかいろいろなケースでバグを生み出す可能性があります。そしてさらに悪いことに、スレッドセーフでないことが原因で起こったバグは再現性が低く、テストで検知するのも単純なバグよりも難しいときました。「ああ、解決までに10時間以上かかった思い出したくないあのバグも、あのバグも、あのバグも、複数のスレッドがひとつのデータにアクセスするのが原因だったな……俺が作り込んだバグじゃないのに……休日はつぶれ、睡眠時間は減り……ウッ」という心の闇を持ったプログラマーは多いのではないでしょうか。これは、ひとつのデータが複数の実行コンテキストに共有されてしまっていることが原因で起こっています。つまり、クラスの外からデータを隠蔽するだけでは、データを守りきることができなかったのです。

あるいはメソッド呼び出し順序依存のはなし

さて、別の話として、以前の実行コンテキストにデータが依存してしまうということについて話しましょう。たとえオブジェクト指向で外からデータを書き換えることができなくなったとしても、同じオブジェクトの中からは書き換えることができます。そしてこれが生む問題のうちのひとつとして、「あっそのオブジェクト、メソッド a を呼んで、そのあと b を呼ばないと例外吐くよ」みたいな問題があります。メソッド b は、実はメソッド a がオブジェクトの内部のフィールドの値を書き換えてそのための準備をしてくれていることを暗黙の前提としている、みたいなやつですね。これは、メソッド b が「先にメソッドaを呼んでおいて」というコンテキストを期待しているということです。ここでも、「実行コンテキスト」の魔の手からデータを守ることに失敗しています。

もちろん、こういう問題は能力のあるプログラマーが十分に気をつけてコードを書くことで回避可能な問題です。でもそんなこと言ったら、「能力のあるプログラマーが十分に気をつけてコードを書けば C 言語で良い」って話にもなります。ここで言いたいのは、「クラスの中にデータを隠蔽するという言語の機能 だけ では、コンテキストからデータを守ることはできなかった」、言い方を変えると、実行コンテキストからデータを守るための機能が「言語レベルでパラダイム(まさに枠組)としてサポートされていない」ということです。

純粋関数型言語の話に入る、その前に

さて、単純にデータをクラスの中に隠蔽するだけでは、データを守りきることができませんでした。となると、もっとラディカルな解決策を考える必要がありますね。そこで純粋関数型言語に注目してみましょう。

と、ここまで来たらなぜわたしがわざわざ「純粋」関数型言語の話をしているのかカンのいいひとは気づいていそうですね。ここでわたしは「変数の再代入が禁止されていること」「データが変化しないこと」に注目したいと考えているのです。関数型言語の中にはデータの再代入が可能だったり値を書き換えてしまうことが可能だったりするものもあり、それらについて考えるときには「データを守る」というのとはまた別の視点から考える必要があると思います。だからわたしはここでは「純粋関数型」に限定して話を進めることにします。

ところで、少し話はずれますが、変数の再代入の禁止とデータが変化しないことの間には微妙な違いがあります。たとえば、変数の再代入を行っていないにも関わらずデータが可変であるパターンを考えてみましょう。こういうのはどうですか?

irb(main):001:0> str = "nyan"
=> "nyan"
irb(main):002:0> str.object_id
=> 70284024921600
irb(main):003:0> str.upcase!
=> "NYAN"
irb(main):004:0> str.object_id
=> 70284024921600

このコードには一度も再代入は出てきません。しかし str の値は小文字から大文字に書き変わっています。さらに、str が小文字から大文字に書き変わっていても、strobject_id は変化していません。つまり、str は「同じもの」を指しているにも関わらず、「データの内容」が書き変わっているのです。

では今度は再代入してもデータが変化してない場合を見てみましょう。

irb(main):001:0> str = "nyan"
=> "nyan"
irb(main):002:0> str_2 = str
=> "nyan"
irb(main):003:0> str = "wan"
=> "wan"
irb(main):004:0> str_2
=> "nyan"

str = "wan" で変数に値を再代入していますが、"nyan" というデータ自体は書き変わっていないので、str_2 には "nyan" が格納されたままです。

純粋関数型言語

さて、話を本線に戻します。単純なオブジェクト指向では「コンテキスト」からデータを守れないのは上に見た通りでした。それはなぜか?それはオブジェクトが可変な状態を持ってしまうからです。

可変な状態は、以前の実行コンテキストや別の実行コンテキストによって「そこに何が入っているか」が決まります。「以前のコンテキストでどういう値がそこに入っているか暗黙のうちに期待している」とか「別のスレッドが値を書き込んだ」とか。でも、変数が一度定義されたらその変数が何をさすかも変わらず、値も書き変わらないことが保証されていたら?

わたしたちはもう「コンテキスト」のことを気にすることはありません。だれも、どこからも、値を書き換えることができないのだら、「もしかしたらこの値は別のところで先に書き換えておいてもらわないとだめなのかも」とか「もしかしたら別のスレッドからこの値いじられるかも」みたいなことを気にする必要がなくなります。

これで、ついにわたしたちはデータを完全に外から守ることに成功しました!!めでたしめでたし。……と、もちろんそうは問屋がおろしません。

純粋関数型言語もそんなに純粋ではない

純粋関数型言語とかの入門書にはだいたい上のようなことが(表現は異なりますが)「書き換えられなくてまじめっちゃ安全だから。最高だから。いちどやってごらんよ」みたいなことが書かれていることが常なのですが、なんかこれってだまされてる気がしません? わたしはだまされている気がします。だって、「一度定義されたらもうデータ書きかわらないのだったら、どうやって "現実の問題" "を解決するんだ!」という話になります。だって、データに外から何も影響を与えることができないなら、そのプログラムは「つねに同じことをして常に同じ値を返すもの」になってしまいます。それって何の役に立つんだよ!!!!!!!!!!

だから、純粋関数型言語にも、「コンテキストによって値が決まるもの」が裏口からこっそり運び込まれています。たとえば Haskell で言えば、モナドがそれです。

純粋関数型言語を学ぶときのひとつの視点

ここで、純粋関数型言語(というかぶっちゃけHaskell)を学ぶときに注目すべき視点のひとつが導きだせるのではないでしょうか。それは「これはコンテキストによって値が決まる部分なので状態を意識しなければならないのか?それともそうでないから状態のことは気にしなくていいのか?」という視点です。逆の方向から見れば、純粋関数型言語は「コンテキストに依存する部分」と「そうでない部分」を言語仕様上強制的に分離することで、「コンテキスト依存のデータ破壊からプログラマを守る」という一面がある、ということが言えるのではないでしょうか。とか偉そうなこと言ってますが、これ実は今私が単にそういう視線で Haskell を現在勉強中であるってだけです!もっと詳しいひとがもっといい視点でいい説明してくれるのを期待してるんだけどだいたいみんなすごい難しいこと言う!難しいものを説明すると難しくなるのあたりまえだし理解できないわたしの側の問題だというのがなおのことつらい!

おまけのはなし

さて、そのようなすばらしく思える純粋関数型言語ですが、ものすごく身もふたもないことを言うと、めっちゃ難しいんですよ。少なくとも今のわたしにはめっちゃ難しい。今までは可変の状態を持ってコンテキストに依存することをあたりまえに考えていたのに、「コンテキストに依存するとバグるから、ほとんどの部分は可変状態もたないことにしたから。むしろコンテキスト持つような部分が例外だから。そういう部分はプログラム全体からきっちり分けて局所的にしろよな」と言語に言われても、そういうふうにコードを書くのは実際めっちゃ難しい。つらい。

そこで、おまけとして、純粋でない言語たちが「コンテキスト依存」に対してどう立ち向かうのか、という視点でいろいろ紹介してみましょう。

D言語とかがそうらしい。これは、「ひとつの実行コンテキストにデータが依存してしまう」に対する解決策として、「このメソッド(関数)が呼ばれる前には状態はこうなってるはずだぜ」ってのをコード内に定義しておくことによって、あやまったコンテキストを作り込んでデータが破壊されることを防ぐぜっていう思想です(と私は理解している)。ただわたし契約プログラミングパラダイムを提供してくれる言語やったことないのでぜんぜん具体的なことわかんない。

Erlang とか akka(akkaは言語じゃないけど) とか。これは「複数の実行コンテキストが同時におなじデータいじってしまう」に対する解決策として、「じゃあ複数の実行コンテキストは同時におなじデータいじれなくしちゃえばいいんじゃないの」っていう発想で、異なる実行コンテキストはデータを共有せずにイミュータブルなデータを受け渡すことにしようね」というパラダイム。じつはこのあたりの話は なぜ akka を使うべきなのか という記事で去年一度書いている。

あと多分リアクティブプログラミングってパラダイムもこのへんの「コンテキストによって値が決まることによる弊害」を解決するという視点で見れるっぽいと思うんだけどわたしリアクティブプログラミングのこと何も知らない……。だれか教えて……。

これからの話

というわけで、この「実行コンテキストにどう立ち向かうのか」という問題は、今まさにいろいろなパラダイムが提唱されている「未解決問題」であると感じていて、プログラマーとして物心ついたときにはオブジェクト指向がすでに普及していたわたしにとって、リアルタイムで感じられる「歴史の動き」なので、わたしはわたしなりの問題意識をもってこの動きを見守っていきたいなぁと思っているのでした。(研究者肌ではないのでわたしはその流れを作り上げる側の人間ではないのがコンプレックスのひとつなんだけど、「私はこういうふうに見てるよ」「こういう見方もできると思うんだよね」ってのをなるべくわかりやすく伝えるのだってひとつの役割だろうって思ってる部分がある)

TMTOWTDI あるいは銀の弾丸はない

そしてこれだけは言っておかないといけない。データを守るという視点から見れば、これらの言語はかなり安全側に倒されていると言えるし、進化だと思う。だけど、それはあくまで「データを守る」という視点から見たときの話だ。たとえばそれによって柔軟性を犠牲にしていたりとか、何かを取れば何かを失うってのが世の常で、「すべてにおいて他の言語より優れた言語」なんて存在しない。

蛇蝎のごとく嫌われる PHP にだって、「だいたいどんなレンタルサーバーにも入ってる」とか、「(質はともかく)人員の確保がしやすい」だとか、いいところはある。root どころか ssh でサーバーにログインすらできないお仕事とか、解決したい問題が「投入した人間の数とその時間をお金に変えること」ならば PHP を使うことだって十分に合理的な判断だと思う(わたしはなるべくその仕事に関わりたくないが)。言語の選択からして、やりかたはひとつじゃない。そして銀の弾丸はない。

いつだって、「解決したい問題」が先にあって、それを解決するための手段はその後にくるものだと思う。だからわたしは「後から生まれた言語が前からある言語よりもすべてにおいて優れてる」なんて単純な史観はもたない。ただ、コンピューターが活躍する分野が増えることで、多種多様な要求や問題が生まれ、それに応じてそれらを解決するための多種多様なパラダイムが生まれてきたし、これからも生まれていくという史観を持っている。そこのところ、誤解してほしくないので一応最後に書いておこうと思う。