2013-03-01

プログラミング言語において、型とはドキュメントである

http://d.hatena.ne.jp/perlcodesample/20130227/1361928810

この記事が話題なんでしょうか。 +Shiro Kawai さんの記事 http://blog.practical-scheme.net/shiro?20130227-equibillium を見つつ、つらつら思ったことなどを書いてみます。

まず、上のリンクの「変数に型がないとどのような型の値が代入されているかわからないという批判に答える」というセクションは、残念ながら答えているとは言いがたいものがあると思います。

第一に、多くのプログラミング言語では [] 等の演算子はオーバーライドできるため、演算子が使われているということを理由にデータの型を推定することはできません。

第二に、->new、というものがperlにおいて構文なのかどうかは知らないのですが、そうでないとすると、この処理によって特定のクラスを返すことは慣習によってのみ決まり、保証されていないでしょう。また、保証していないことを利用したテクニック、というのもあるはずです。ついでに言うと、クラスの名前というのは多くの場合たいして大事なことではなく、どのようなオペレーションが許されているか、ということが大事なのではないかと思います。

第三に、Clientがuaで何を返しているか、どうやって調べればいいのでしょう。

第三を掘り下げてみましょう。世の中にはクローズドソースのライブラリというのがあって、調べることができないこともあるかもしれません。が、スクリプト言語の世界ではそういう事例は少なそうなので、そうでないとしましょう。

でもオープンだとしても、本気でこれ、言ってるんでしょうか。Clientを見たら、内部の長い関数を呼び出した結果を持っているかもしれません。別のライブラリに依存しているかもしれません。場合によって異なる型のオブジェクトが返されているかもしれません。ひとつひとつ、丹念に追うのでしょうか。

Ruby on Railsを、大昔(1.4.xくらいのころ)に触ったことがあるのですが、当時に似たような不満を感じました。RoRではオプショナルな引数を Hash で渡しますが、そもそも Hash のキーはシンボルなのか文字列なのか(どちらでもいい)、どのようなキーが許されるのか、といったことがさっぱりわかりませんでした。しかも、オプションの一部をある箇所で使い、残りは別のライブラリ関数が使い、などとしているため、混迷の度は深まっています。RoRのコードをそれなりに読んでいかないといけなくてフラストレーションが溜まったものです。

こういった問題は、ドキュメントを書くことで解決されます。クローズドソースのライブラリの場合にも、たいていはしっかりしたドキュメントがあったり、サポートがちゃんと答えてくれたりするでしょう。

そして、個人的な見解としては、きちんとしたドキュメントでは、APIはどのような型のデータを受け取り、どのようなデータを返すのか、ということを記述しています。Rubyのドキュメンテーションプロジェクトでは、メソッドの引数や返り値の型の記法が整備されていました。 Closure Tools のコンパイラでは、Javadocのような記法でパラメータのドキュメントを書くことが推奨されていますが、コンパイル時にこの型を調べ、チェックを行う機能があります。

しかし、ドキュメントに記述するのであれば、しかも記法を整備しながら、どうして機械にも処理できるようにしないんですかね。そうしたら便利になりませんか。プログラミング言語の機能に組み込んだほうがいいんじゃないでしょうか。

つまり、どのようなデータをわたす必要があり、どのようなデータを受け取ることになるのか、ということがプログラミング言語内で表現し、利用することができる。というのが、静的に型付けされた言語の「利点」なのです。

もちろん、この表現は問題をかなり単純化しています。何にせよ、Javadocのようなものが必要であるということは、型だけでは表現能力が足りないということを示唆しています。でも、それはその特定の型システムが弱いという問題であり、特定の言語がしょぼいという話であって、型付け自体の問題ではない可能性があります。たとえば、「nullを渡してもいいよ」「nullを渡すと例外が飛ぶよ」という記述をしておかないとわからない、という問題があるとしたら、「nullになりうる型」といった型修飾がなぜないのか?と考えるべきかもしれません。

インタフェースを事前に定義しておかなければならない、継承関係をきちんとしておかなければならない、などなど、静的型付け言語になされる批判は、単に特定のプログラミング言語の問題である可能性もあります。たとえば、Go言語はducktyping的な型付けができますね。

ここまでのまとめ:

  • 型は機械処理可能なドキュメントとして有用であると思われる
  • 型付けをしない言語から見た静的型付け言語の欠陥は、単に特定の言語の型システムの欠陥でしかないかもしれない
  • まぁだからといって、既存の言語同士で比較した時に、どのシステムがいいのか、なんてのはわからないし合意が取れるようなもんでもない

---

で、段階的にインタフェースが変わっていくようなライブラリがいる場合に、どうするかっていう話なんですが。

これはケースバイケースではないかなぁと思います。いくつかパターンはあるでしょう。

第一に、サードパーティ製のライブラリであれば、たんに先端に追随しないというのが一番単純で、コンサバだけど、ありそうな方法ではないかと思います。インタフェースは固定であり、セキュリティフィックス以外の変更は取り入れないようにして、変更しないようにする、というものですね。そんで、適当な周期でアップデートをかけ、その段階で問題をまとめて直す。

第二に、ラッパをかましてラッパ部分で変更をうまく吸収するという手もあるでしょう。これはラッパの出来に依存しています。ライブラリで、大概のパラメータがツールそのものによって決まるような環境では、ラッパを使う手法はけっこううまく機能すると思います。

それから、引数にパラメータオブジェクトを使うことでオプショナルなパラメータを制御する手法が考えられます。デフォルト値の変更やパラメータ数の変化はこれで対応できます。ファクトリを使って、パラメータの漸次的な変更を許すようにする手もあります。

もちろん、それぞれ良し悪しがあります。バージョンを固定してしまうと最新の機能が使えなくなってしまうし、アップデートの周期が長くなると、アップデート時に大変な目にあって泣くことになります。型によって検証できる、というのは理想論で、アップデートによって一見無関係な箇所でクラッシュするという事例もよくあることでしょう。

パラメータオブジェクトを利用する場合も、大きな問題がありえます。最近 ChromeOS で起こった問題としては、たとえばこんなものがあります。ウィンドウを作るとき、 Widget::InitParams というオブジェクトを作って渡して初期化します。ところが、とある事情からこの InitParams に context というフィールドが足されました。この値はデフォルトでは NULL なんですが、 NULL のままにしておくとたいていの場合にクラッシュするようになりました。このためにクラッシュ率は相当上がったように記憶しています。

この場合、どうすべきだったのか?というと、おそらくは InitParams を作るときに context を指定しなければならないように変更すべきだったのでしょう。必要ないケースでは明示的に NULL を指定させるというインタフェースに書き換えていれば、この変更をコミットする段階で、 InitParams を作る箇所をすべて書き換える必要があり、事前にかなりの問題を検出できたはずです。

ところで、この話はもちろん C++ なんですが、これはべつに言語に依存しないし、型が静的であるか、動的であるか、といったことにも何ら依存しないのではないでしょうか。動的型言語であっても、オプション引数で、このオプションを指定したならばこれも指定しないといけないとか、このオプションとこのオプションは同時には成立せず両方指定した場合はこちらが優先されるであるとか、そういうことから発生するようなバグというのは非常によくある話であるように思います。

また、動的にしか型付けをしない場合でも、インタフェースの変更によって arity が変化した場合は、多くの場合コンパイルに失敗したり、ひどい問題が起きることはよくあるのでは、と思います。けっきょく、同じような苦労は、言語の特質にかかわらず発生するように思います。

そうだとすると、動的型言語のほうが「時定数」が長めになる、というのは、ほんとうかどうかはもうちょっと考えないといけないのではないかなあと思います。