ミスを減らす
毎日書く普通のコードでなるべくミスを減らし、デバッグを簡単にして、より多くの時間を楽しい事(キーボード叩いたり汎用関数作ったりリファクタしたり)に費やしたくて最近実践していることをメモします。一つ前の燃えてるエントリの補足(不変指向の利点の説明)も兼ねているので話があっちこっちぶれてしまいました。
順番への依存を減らす
「Xを実行してからYを実行するとおかしくなる」*1とか「Yを安全に実行する前には必ずZを実行しなければいけない」*2とかは絶対にミスるのでなるべくなくす。後者についてはラッパを作ってその中でアトミックに実行するとかで解決できるけど、前者はミュータブルなもの(オブジェクト)を使っている限り至る所で発現し、根本的な解決は困難。以降のセクションで関数型の考え方を取り入れてこれを解決していきます。
状態を減らす
多くのプログラムは状態を持ちます。ある関数のようなものにプログラム中さまざまな場所で同じ引数を適用したときに違う結果が帰ってくる可能性があるなら、その関数のようなものは状態を内包しています。オブジェクトはそもそもが"状態のカプセル化"です。スロットの書き換えを行うという事はメソッドを呼んだ時の結果が変わることに直結します。グローバル変数も状態です。
関数型の考え方(不変指向)でプログラミングを行わないと、こうした状態を内包した何かがプログラム中に散乱します。例えばMVCではモデルもビューも状態を持ったオブジェクトで、モデルもビューもそれぞれ複数あるので、状態地獄です。
状態が入り込むと、順番への依存が激しくなりミスが増えます。
var xs = [1,2,3,4,5,6,7,8,9]; some_function1(xs); xs.some_method(); some_function3(xs); test("sum up xs", function () { strictEqual(_.reduce(xs, _['+']), 45, "1+2+3+4+5+6+7+8+9 = 45"); })
このコードで、xsが値で、some_functionやsome_methodが関数ならテストは通るはずです。しかしxsはオブジェクトなので、some_functionやsome_methodの中で何をされるかわからず、テストが通ることを保証できません。some_functionやsome_methodが状態を持っていたらなおさらなにが起きるかわかりません。状態を複数持つプログラムで鼻持ちならないことが起こった時は、コードから何が起きるかを予測できないのでデバッガを開いてブレークポイントをポチポチ貼って実際に実行して変数の値を確認する必要があります。
状態は1つ以下に抑えるとコードの字面から起こりえる状況の予測がしやすくなり、考える事も減ってミスも減ります。状態を1つ以下にするには、一つ頭を再配線しなければいけないことがあります。次のセクションでこれを見てみましょう。
アイデンティティと状態の分離
オブジェクト指向や構造化プログラミングで、変数やオブジェクトという形で状態地獄が出来上がってしまうのは、これらの仕組みが、2つの全く別のコンセプトをくっつけてしまっているからです。別にくっつけようと思ってくっつけたわけではなく、ポインタでメモリを直接弄っていた時代の遺産が延々と引き継がれてきた結果として多くの言語において勘違いを強制される状況になっています。その2つとは、アイデンティティと状態です。
状態については既に触れました。アイデンティティは日本語にすると同一性で、あるデータxとあるデータyが同じ物であるかの基準になります。identityという字面から推察される通り、データベースとかWeb APIとかを使うときに必ず目にするidの仲間です。
オブジェクト指向ではアイデンティティとしてポインタが使われています。つまり、情報として同じ物であるかをメモリ上で同じアドレスにあるか否かでしか識別できないんです。そりゃそうですよね。オブジェクトの中身はどんどん変わっていきますから他に同一かを判断する方法なんてないですよね。事実(情報)というのは、メモリという場所に結びついていて、そこの内容が変われば事実も変わります。
That's just bullshit!!!
//山田さんがいます。山田さんのメールアドレスは2013年6月13日の今日、yamada@example.comです。 yamada = new Human({name: "Yamada", email: "yamada@example.com"}); util.inspect(yamada); //=> {name: "Yamada", email: "yamada@example.com"} //2013年6月20日、山田さんが新しいケータイを買いました。ついでにメアドも変更したそうです。新しいメアドはxxx_yamada_love_hanazawa_forever_xxx@example.comだそうです。 yamada.setEmail("xxx_yamada_love_hanazawa_forever_xxx@example.com"); util.inspect(yamada); //=> {name: "Yamada", email: "xxx_yamada_love_hanazawa_forever_xxx@example.com"} /* WHAAAAT THE HELL HAD JUST HAPPEND??????????????? */
6月13日から6月20日までのシンプルなメールアドレスを使っていた山田さんはどこにいったんでしょう???? これを実世界*3に当てはめてみると不自然さに気付きます。実世界で、僕らはいきなり変なメールアドレスを使い始めた山田さんを見て笑うはずです。僕らがこれを笑えるのは前のメールアドレスを知っているからです。山田さんが真面目なメールアドレスを使っていたという事実が変わらないからです。オブジェクトの世界では長期記憶が直接書き変わってしまうので僕らは山田さんの新しいメールアドレスを見てもそのギャップを笑えないのです。この事実の書き変わりが順番への依存を深刻化させ、ミスを生み出す原因です。
少し脇道にそれましたが、実世界の話を挟んだことではっきりしました。僕らはアイデンティティと状態は、それぞれ別の物だと知っているんです。事実は変わりません。事実は時間に結びついています。状態は、時間上の一点での最新の事実です。アイデンティティは事実が連なったタイムラインです。
この正しいモデルを実際にプログラムで使うにはどうすればいいのでしょうか。パッと思いつくのは再帰を使う方法です。実際、多くの関数型言語で作られるゲームは、ゲームループを状態の値を受け取って、これとユーザのインプットをもとに新しい状態を作って自身を再帰呼び出しする関数として実装しています。ゲームループの例のような再帰で表現し辛い場面では、アイデンティティだけを管理する変数Stateを一つだけ用意して、これに実行中に更新されうる値を全て保持したイミュータブルな辞書を持たせて、状態を変える度にこの辞書を生成してStateの参照をこの新しい辞書に張り替えることで正しく表現することができます。WebFUIやPastaはこの方法を発展させて使っています。Clojureのatomとかrefとかもこういう使い方ですね。以降この状態の辞書のことをStateと呼びます。
/* 必要最小限の状態管理API、もう少し使いやすい物はPastaを参照のこと。 */ function App (initial_state, watcher) { var state = initial_state; return { patch: function (patch) { var new_state = Object.freeze(_.conj(state, patch)); watcher(new_state, state); state = new_state;//Stateの参照を張り替える。Stateの中身を直接いじるわけではない! }, deref: function () { return state; } }; }; var app = App({yamada: {name: "Yamada", email: "yamada@example.com"}}, function (newState, oldState) { //Stateが変わるときに呼ばれる //この中は再帰のときとほとんどおなじ //Stateは値なのでコピーとかせずに保存してあとで使ったりできる //ここではアプリの見た目を弄ったりする. Stateのみに依存すること! }); //山田さんがいます。山田さんのメールアドレスは2013年6月13日の今日、yamada@example.comです。 yamada20130613 = app.deref().yamada; //=> {name: "Yamada", email: "yamada@example.com"} //2013年6月20日、山田さんが新しいケータイを買いました。ついでにメアドも変更したそうです。新しいメアドはxxx_yamada_love_hanazawa_forever_xxx@example.comだそうです。 app.patch({ yamada: _.conj(yamada20130613, {email: "xxx_yamada_love_hanazawa_forever_xxx@example.com"}) }); yamada20130620 = app.deref().yamada; console.log("山田さんは6月13日のメアドは" + yamada20130613.email + "です"); //=> 山田さんの6月13日のメアドはyamada@example.comです console.log("山田さんは6月20日のメアドは" + yamada20130620.email + "です"); //=> 山田さんの6月20日のメアドはxxx_yamada_love_hanazawa_forever_xxx@example.comです
アイデンティティと状態を分けて考える事の利点は、プログラム中考えなければならない状態が一つだけにできるということです。この利点は他の記事やPastaのREADMEでも散々強調してきたことなのですが、アプリケーション自体を値として扱えることです。アプリケーションのStateからアプリケーションの内部状態と見た目を完全に再現できるようセットアップしておけば、Stateはただの値なのでjsonにでもシリアライズすればほぼどこにでも保存できて、ワイヤに乗せて人に送りつけたりなんでもできます。
アプリケーションの状態を把握するにはこのStateを見るだけになるのでバグの原因究明が簡単になります。状態が散らばっていたらどの状態に依存してこのバグが出たのかを探す必要があります。ミスは必ず起こるので、修正は簡単な方が良いです。
ミュータブルなデータ構造(オブジェクト)を使わない
これまで見てきた事を総合すると、ミスの多くがミュータブルなデータ構造(Cの構造体とかオブジェクト)を使っていることから生まれるので、これを使わないようにしようという主張になっていたかと思います。オブジェクト指向の「情報を破壊し続けながらプログラムが進行する」という特徴以外は触れませんでしたが、興味のあるむきは「総称関数」「multimethod」「Clojure protocol」「型クラス」とかで検索してみるとオブジェクト指向の利点だと考えていることがオブジェクト指向に特有の機能ではないことがわかるかと思います。
なんかほんとに書きたかったことからズレすぎていつもと同じような内容になってしまいましたがこれ徹夜して書いてるので一回保存して寝て起きてから見直します。