2013-03-31

マリー・ブレナン『竜の博物誌』

さいきんこんなの読んでました。 A Natural History of Dragon: A memoir by Lady Trent というタイトルのファンタジー。タイトルと表紙で買ったようなものですが、なかなか面白かった。でも、ちょっと惜しい。

ビクトリア朝期のイギリス、のような世界を舞台に、竜族の科学的研究で著名な学者であるレディ・トレントが書いた回顧録、という体裁で、若い頃の冒険を描いた本。この設定と表紙ですでに勝利が確定したような感もないではなかったのだけれど。

地方貴族の家柄に生まれ、生まれてはじめて竜に出会い、長じて生物学に興味を持つ「虫愛する姫君」になって、学問書を読みふけり、嫁の貰い手がなかったところで伴侶に出会って……という第一部はけっこう面白くて、これは期待が持てるなと思ったのですが、第二部以降はわりとふつうのファンタジーになってしまった感じ。

結婚相手の敬愛するヒルフォード伯と知己を得、ヒルフォード伯の探検についていくことになるまでが第一部で、第二部以降はずっと探検先の土地でどういうことがあって、何をして、みたいな、ふつうの冒険旅行の物語になってしまう。冷静になって考えてみると、回顧録のわりに会話文が多いし、どうも本のもつ体裁とうまく合致していない気もする。

ただ、このビクトリアンファンタジーがつまんないかというと、まあふつうに面白いわけです。いろんな土地を探索して、竜を発見したり解剖したりするいっぽうで、受け入れる側の村人とのコミュニケーションがあったり、遺跡を探索したあとに、どうやら宗教的禁忌に触れたということで軋轢が生じたり、野盗のような連中に遭遇したり……、そんななかで、どうも普段は人間と関わることの少ない竜たちが最近は人間を襲っているらしいという問題に、徐々に近づいていく。

満足しましたし、表紙は魅力的だと思いますが、総合的には星5つ満点で3-4ぐらいかな。

2013-03-11

古くて新しいGUI座標系の型の話

こないだからの話題にちょっと関連する話として、さいきんChromeOSの仕事で手伝っていたマルチディスプレイ対応の機能について解説しよう。

現在販売されているChromebookには何らかのかたちで外部ディスプレイに接続する機能があるが、古いバージョンではミラーリングしかできなかった。それじゃ役に立たないので、複数のディスプレイをくっつけるような拡張デスクトップの機能をサポートした(まぁ、今時どんなOSにだってはじめっから入ってるような機能ではありますが)。Chromeのバージョン25以降でこの機能がオンになっている。ところが、この機能がなかなかの難産だった。

もともとChromeはブラウザだったから、そのGUI部分はウィンドウ基準の座標系だけを気にするようなつくりになっている。一方、ChromeOSではAuraという独自のウィンドウマネージャを実装したが、Auraにはデスクトップがあるので、デスクトップの座標系を持たなければならない。この場合、マウスイベントなどのイベントは画面座標系でやってくることになる。そこで、これをウィンドウ座標系に変換して渡す。GUIプログラミングではごく当たり前のつくりだ。

Auraのウィンドウは親をたどっていくと最終的にはRootWindowというオブジェクトにたどり着く。このオブジェクトが実際のX11上のウィンドウと対応していて、マウスイベントやキーボードイベントなどのX11イベントを受け取り、Auraのイベントに変換され処理が進むようになっている。

ここまでは何の問題もないと思う。だが、これをマルチディスプレイ化することで、複雑な問題がいろいろ出てきた。マルチディスプレイ環境では、複数のRootWindowが存在し、それぞれのRootWindowが各ディスプレイに対応するというつくりになっている。するとどうなるか?

まず第一に、スクリーン座標系と別にグローバルな座標系が必要になる。スクリーン座標系は画面の左上を(0, 0)とする座標系だが、グローバルな座標系はプライマリなディスプレイの左上を(0, 0)とする座標系だ。ディスプレイが横につながっていたら、セカンダリのディスプレイの左上は(0, 0)とは限らない。現実的にはスクリーン座標系は便利だし、大半のコードはスクリーン座標系で動作するようになっているから、両方を持って適宜変換することになる。これは、まあいい。簡単ではないが、どんなOSでも発生する問題ではある。

次に、ネイティブの座標系とAuraの座標系の不一致という問題がある。ChromeOSは下位レイヤーにX11を使っているが、X11が配置するウィンドウのレイアウトと、Aura上のRootWindowの論理的なレイアウトは一致しない。Aura上でRootWindowの接続を左右から上下に移したとしても、X11レベルでは何も起きず、Aura上の論理関係だけを更新する。あらゆるX11のイベントはネイティブ座標系で渡ってくるが、RootWindowではこれをAuraの座標系に変換しなければならない。

さらに問題をややこしくしていたのがChromebook Pixelの存在だ。Pixelというのは最近発表された高解像度ディスプレイの製品だが、高解像度なため、2倍表示させている(MacのRetinaのように)。つまり、物理的には2x2の4ピクセルを論理的には1ピクセルとして扱い、本来の解像度は2560x1700だが、論理的な画面サイズは1280x850として表示するようになっている。このために高精細な画像になり、画面はものすごく綺麗だ。綺麗だが……当然ネイティブ座標系は物理的なピクセル値でイベントを生成する。これをAuraの論理的な座標系に移す際に、適宜2で割ったり、倍にしたり、といった処理が必要になる。

基本的には、すべてRootWindow内で物事を完結させ、外部からはこういうややこしい問題は見えないようにする。当然、そうなっているべきだし、そうしている。が、そうするためには、もともと単一ディスプレイしかなくてネイティブ座標系、グローバル座標系、スクリーン座標系がぴったり一致した世界だったところに変換を噛ませないといけない。

もちろん、すべてを変換すればいいだけの話だし、テストを書いて問題が再発しないようにしている。そもそも大元のコードパスを直すのはそれほど大変じゃない。だが、意外なところに抜け道があったりして、検証を難しくしている。しかも、当然だが変換を忘れてもたいていの場合はそこそこ動く。だが、最後に完成度を高める部分はけっこう大変だ。外部モニタをつないで、ディスプレイの配置を変え、プライマリディスプレイを入れ替え、ウィンドウじゃなくてタブを直接ドラッグして、高解像度ディスプレイから普通のディスプレイにドラッグして移そうとした時にだけなんか挙動が変、みたいなバグを直したりしないといけない。どこかで変換しわすれが発生しているのだが、どこなのかはすぐわかるものでもない。おかしくなった箇所と変換の箇所は遠く離れている、ということはよくあるからだ。

あるとき、2xディスプレイがらみのバグを直していた時に、いいかげん嫌になって、型を変えるべきなんじゃないかという話を同僚としたことがある。いまさらのタイミングだったのでそうはならなかったし、その判断は間違いではなかったと思うが、ここからは実際を離れて理論面を考えてみたい。

理論的には、ネイティブ座標系とかグローバル座標系とかスクリーン座標系とかいうのは、本質的には異なった型だ。インタフェースは同じ、xとかyとかだが、意味が違う。本質的に異なる型なので、プログラミング言語上でも異なる型にしてしまうと、こういう問題はかなりの部分は解決される。変換わすれのようなミスはコンパイラがすべて見つけてきてくれるからだ。

インタフェースが同じだからといって同じ型だと思ってしまったり、型のチェックを持たないと、こういう問題を見つけることは難しい。したがって、動的型言語ではこういう問題について、入出力のテストをいっぱい書いて問題が起きないようにする。だが、実際にこのような変更を途中でするときに、すべてのテストを書き切るのは難しい。問題を発見するたびにテストケースを足して漏れを直すのだが、どうしても後手に回っている感は否めない。

けっきょく、これは時定数の大きな問題だ。ここで異なる型を導入すると、いきなりぶっ壊れてしまうので、おいそれとはできない(Chromiumの規模のコードベースのあちこちで使われている型だから、そう簡単には変えられない)。機能自体も作るのにそれなりに時間を要するし、インタフェースもその間にはころころ変わるから、事前にこうと設計するのも難しい。後出しじゃんけん的には、プリプロセッサを使ってみるとか、もっと初期の段階で型を分けておくべきだったとか、いろいろ思うところはあるが、現実的であったかどうかは難しいところだ。結局、そういうわけで、僕らもテストを書いて対処した。だがそれでも……とたまに思う。

common-lispではいい手はあるのかな? generic functionやオプショナルな型チェックはできるだろうし、マクロを使って型チェックを導入する(しかもリリースビルドではチェックを省略するとか)できるだろう。が、そう簡単にはいかないのではないか?という気がする。どこにチェックするかを指定する、ということは、テストを書くのと同じぐらい大変だ。型レベルの変更は、コンパイラが網羅的に検証してくれるので、楽だ、というのがここでの要旨なので。

まとめ。
  • 世の中には、インタフェースや実体は同じだが、意味が違うために異なる型になっているモノというのがある。静的型の検証は、そういう「意味」を表現できる
  • もちろん、意味はテストでも表現できる。テストのほうが表現力は柔軟でもある
  • だが、型の検証は自動的かつ網羅的なので、便利な面もある
と思うけど、どうでしょうか。

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 が変化した場合は、多くの場合コンパイルに失敗したり、ひどい問題が起きることはよくあるのでは、と思います。けっきょく、同じような苦労は、言語の特質にかかわらず発生するように思います。

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