2012-09-28

Ernst Cline "Ready Player One"

嫌いになれない。

ギーク小説、というジャンルがあると思う。ギークな作家が、自らの体験を交えて書くようなタイプの作品だ。具体的な作品名やキャラクター名や設定が飛び交い、そこにどっぷりと「浸かっている」ような読者を喜ばせて、身悶えさせる。そういう意匠に凝りに凝った作品、っていうのが確かにある。
たとえば、秋口ぎぐるの『ひと夏の経験値』は、90年代にテーブルトークRPGをやっていた中高生というごく狭い人々にとってはただ事ではない迫真さがある。そういう狭い層にはいかんともしがたいというものがある、というタイプの小説というのは、たしかにある。

本書 "Ready Player One" も、そういう「ギーク小説」のひとつで、80年代アメリカのデジタルゲーム・アニメ・ゲーム・まんが・ポップカルチャーに強く依拠している。僕よりも少し年上の世代がストライクなのかな。いずれにせよアメリカの本なので、日本人にはそこまで強くは訴求しないかもしれないけれども、そういう道具立てがすごい。

21世紀のなかごろ、稀代のゲームデザイナーとうたわれたジェームズ・ハリデイがとある遺言のビデオレターを残す。ハリデイはもともと重度のおたくで、優れたゲームセンスを持ち、友達とゲーム会社を立ち上げて大成功する。だが21世紀初頭になって、MMORPGとSNSをくっつけたようなOASISというシステムを構築し、これまた大成功。この作品の時代には、このOASISというのがインターネットと同義になっていて社会インフラと化しており、なおかつそれが娯楽でもある、みたいな状態になっている。

ハリデイは自らの莫大な富を、このOASISのどこかに隠したのだという。それがビデオレターの遺言の内容だった。天涯孤独だった彼には肉親はおらず、富は宙に浮いている。ハリデイが隠した秘密を解き明かし、ゴールまでたどり着いたものがこの富を得ることができる。ヒントはないが、ハリデイの愛した80年代ゲーム・ポップカルチャーが強いモチーフになっていることが暗示されている。

これが発表されてからさあ大変、誰も彼も、猫も杓子も大企業も、この「イースターエッグ」を探し求める旅に出た。だがなかなか見つからない。手がかり一つとっても誰もわからない。人々はハリデイの嗜好から問題傾向を推測しようと、80年代ポップカルチャーの研究を深め、そういったファッションが大流行していく。だが、それでも見つからないまま5年が経過。人々の興味も薄れかけてきたそのころに、ついに変化がおとずれる。無名のハンターの名前が、突然スコアボードに躍り出たのだ。それが主人公である「ぼく」のことなのだが……。

どう思いますか、この筋立て。

似たようなことを思う人もそれなりにいると思うので、はっきり言わせてもらうと、こういうセカンドライフみたいなSNSが世界を席巻するみたいな世界観には、ぶっちゃけて言えばうんざりする。しかもこの本は刊行が2012年。2012年に出た本とは思えないほど、この小説内のサイバーカルチャーは時代錯誤感にあふれている。3DCGのSNSもそうだし、リアルライフとヴァーチャルライフの単純な二分もそうだ。いくらなんでもこれはないんではないか……などと思うのはもしかすると僕がMMORPGなどの文化に親しみがないからであって、スカイリムとかをやっている人にはこういうのもリアリティがあるのかもしれない。が、ともあれぼく個人の感想を正直に言うと、特に序盤は投げそうになるほど、この辺の感覚の齟齬が大きく、読んでいてキツいものがあった。

そもそもなぜ、SF作家は3DCGバーチャルリアリティがそんなに好きなんだろ? むしろ今となってしまうと、こういう世界にはリアリティを感じなくなってしまった。個人的には、3DCGのバーチャルリアリティ空間が採用されるのは、小説が書きやすいからだろうと理解している。チャットや掲示板だけで描写すると、あまりにも前衛的な文学になってしまう。大衆娯楽文化としては、会話の途中の細かい描写を描かないと読みづらくなってしまうし、そうするには手っ取り早いのは3Dにしてしまうことだ。そうすれば、目線を逸らしたり、身じろぎをしたり、そういった何気ない、言外のジェスチャーを描写できる。この本がそうなのも、案外とそういった理由だったりするのかも。

ともあれそういうわけで、ベースラインとなる設定は個人的にはかなりキツい。だが、娯楽小説としては普通によく書けており、読みやすいし、さらさらと読むぶんには特に問題を感じない。そんでもってむしろ、本書の核心はむしろポップカルチャーやゲーム・おたく文化の部分にある気がするので、ここにあまりこだわっても仕方ないのかもしれない。SFファンというのは基本的に設定にうるさいので、気にしてしまうわけだが……。

だが、そういう「きつさ」を乗り越えて読み進めると、やっぱりどうしても、嫌いになれない自分に気づく。なかに登場するゲームなどのネタの大半は、厳密にはよくわからんのだけれども、肌感覚でちょっとわかる。そしてその「わかっている感じ」というのはたまらない楽しさではあり、それこそが「ギーク小説」の(ギークにとっての)楽しみではあるわけだ。たまに登場する日本文化のネタはときどき微妙に間違っており(ウルトラマンの身長が156フィートだとか……アメリカではそういう設定なのだろうか?)、そういう感じでニヤニヤしたりする楽しみもある。それにまた、クライマックスの戦いで主人公がレオパルドン(東映スパイダーマンのやつ)に乗り込んで、「チェンジ・マーベラー!」とかやってから出撃してメカゴジラと戦う、みたいなところではやっぱり日本人のおたくとしては気分はそれなりに盛り上がり、「やっぱ嫌いにはなれないよなあ」としみじみしてしまうのだ。

つまるところ、この本は「ギーク小説」としてよく書けている。ギーク小説は、もちろん普通の小説として読んで楽しむこともできるけれども、中に散りばめられたギークネタを拾って楽しむというのがやっぱり楽しい、というタイプの小説だ。ぼくもそういう小説は好きなのだ。だから、どうにもこういう話は否定しにくいものがある。

まあ、苦労して読むほどの価値はないけれども、まんがいち訳されることがあったら読んでみたらいいんじゃないかと思います。

2012-08-29

chromiumの継続的インテグレーション

最近はどうもJenkinsとかTravisCIとかいうのが話題みたいなのだが、使ったことがないのでよくわからない。だがどうも漏れ聞く話を見ていると、こういうのは継続的インテグレーション(CI)と呼ばれていて、だいたい自分の社内プロジェクトでも似たようなことをやっているらしい。そこで、Chromiumがどういう環境でCIしているか、ということを簡単にまとめてみたい。あらかじめ書いておくと、名前が違うだけでだいたい普通です。

BuildBot
Chromiumは普通のクライアントプログラムなので、ビルド環境の想定がけっこう複雑だ。Windows/Mac/Linux/ChromeOS(最近はAndroidなどのモバイル環境)のようにプラットフォームは多岐にわたるし、同じプラットフォームでも様々なビルドコンフィグレーションがある。テストも数が多く、ローカルに走らせておくのは時間がかかる。

BuildBotは様々なビルド環境で実際にビルドし、テストを実行するフレームワークだ。複数のマシンインスタンスがそれぞれ微妙に違ったコンフィグレーションでビルドを行い、テストを実行してくれる。どこかに問題があれば即座に問題を報告し、ツリーを閉じる。

ツリーが閉じられると基本的にはだれもコミットができなくなる。問題のレポートは、問題となりそうな……つまりそれまでビルドが通っていたのに新しくビルドが壊れる状態になるあいだにコミットされたパッチの作者(たち)に送られる。問題を引き起こした開発者は、単純ミスですぐ直せそうなら自分で直すが、たいていの場合はrevertする。

鵜飼さんの講演にもあったように、Chromiumでは平日では平均で7分半にひとつといった頻度でパッチがコミットされている。このため、一つ一つのパッチをすべて段階的には確認するほどのリソースはかけず、複数のリビジョンをある程度はまとめてBuildBotで確認している。頻度が高いというのは、ツリーが閉じられたらものの数分で誰かが気づくということだ。ツリーを壊すと「お前の変更で壊れてるっぽいんだけど」と文句を言われ、せっつかれることになる。そんなときは冷や汗を書きながら、ごめんごめん、いまrevert中なんだ、などと答えたりする……。

sheriff
たいていの場合、ツリーの閉じられ方からはだれのどのパッチが問題なのかはすぐわかる。開発者は、壊れた状況から自分ぽいな、ということはすぐわかるし、壊したらすぐ自分で対処する。……とはいえ、完全に開発者の自治のみというわけでもなく、BuildBotとツリーの状態を守る専任もおり、sheriffという。

sheriffは専任といっても、実際にはローテーション制が採用されていて、担当になると数日間、ツリーの監視を行う。デベロッパは世界中にいるので、ローテーションも在籍地の時差を考慮して複数人が割り当てられており、さすがに夜寝てるあいだとかにはほかのsheriffが仕事をしてくれる。

sheriffの仕事は実際問題としてはそんなに忙しくはない。ふだんはのんびりと、メインの仕事をしていたりする。だが、ひとたびツリーが閉じたら問題を調べ、だれのどのパッチが問題を引き起こしたのか同定する。そしてその開発者とコンタクトを取って、修正が容易なら修正するし、ダメならrevertする。

炭鉱の庭師という記事を読んで、gardenerはsheriffより大変だなと思った。ツリーのcloseは自動的なためsheriffの仕事はリアクティブだし、上にも書いたように、多くの場合には何がツリーを閉じたのかは自明なので、負荷は大きくない。ごくまれにコンパイラのバグが顕在化して大混乱に陥ることもあるけれど……。sheriffはどちらかといえば、「ツリーが閉じててコミットできん!」と怒れるほかの開発者の代理に文句をいう仕事であり、仮に当該の開発者はもう家に帰ってしまったとかいった理由で反応がない、などの理由で連絡が取れない場合に責任をもってrevertする仕事だ。

trybots
BuildBotとは別にtrybotというのもある。trybotは、自分のパッチをコミットに確認するための仕組みだ。trybotも同じように、様々なビルド環境とコンフィグレーションを持っていて、それぞれ名前がついている。winとかmacとかlinuxとかlinux_chromeosとか。そしてそれぞれ適当なリビジョンのソースツリーを手元に持っている。開発者がコマンドを発行すると、パッチを適用し、実際にビルドしてテストを走らせ、結果を教えてくれる。

コミット前に適宜trybotを走らせておけば、パッチの健全さはわかる。Chromiumの使っているRietveldには改造が施されていて、trybotを走らせた場合にはその実行結果が表示され、わかるようになっている。

trybotはBuildBotほどバリエーション豊かじゃないし、ごくまれにtrybotをすり抜ける問題もある。だが99%の問題はtrybotでふるい落とすことができると思う。パーセンテージの数字は適当 :-P

commit queue
trybotは確認のためのインフラであって、それ自体は自動化はされていない。trybotの手間を惜しんでしまうひとがいることもありうるし、まれに必要なtrybotを走らせておらず、うっかりビルドが壊れるということがある。たとえば、ChromeOSでしか使わないと思っていたら、とある特殊なコンフィギュレーションのMacでも使われることがあることを見落としていたとか、標準的なC++を自分では書いているつもりがVCでだけコンパイルエラーになるとか。私もいちど、ごく普通にあるクラスにkInvalidIDというクラス変数を足したところ、実はCarbon内でkInvalidIDは#defineされているためMacでだけコンパイルエラーになる、などという自体に遭遇してひっくり返りそうになったことがある。そんなの知らんがな……。

閑話休題。

そんなわけで、commit queue (CQ)がある。開発者がパッチにフラグをつけるとCQはそのパッチをひと通りのtrybotにかける。それで全部でうまく行ったものだけがコミットされる。これによってくだらない問題を回避できる。上で書いたMacの問題も、まさにCQによって発見された問題であって、これがなければうっかりビルドを壊してしまっただろう。タイミングによっては直せず、revertされてしまったかもしれない。

もっともCQにも弱点はある。というのは、一般的なtrybotでの確認しかしないから、特殊なビルド環境で開発している場合は自己責任でtrybotを走らせるしかない。それに、特定のプラットフォーム以外ではビルドすらされないような変更では、CQを走らせるのはたんに時間と計算リソースの無駄でしかない。

コードレビューとコーディングスタイル
コードレビューいろいろでも紹介されているように、Chromiumのコードレビューはコミット前に行う。デベロッパは自分でレビュワーを見つけ出し、指定する。レビューは(trybot等の表示の改造が施された)Rietveldを使っている。

レビューは基本的にだれがやっても良いし、だれに依頼しても構わない。ただし、ディレクトリごとにオーナーがおり、そのパッチで関連するファイルのすべてのディレクトリのオーナーからの許可なしにはコミットはできない。とはいえ、オーナーの数は限られていることも多いから(たとえばui直下のオーナーは3人しかいない)、そうした場合には、そのファイルやパッチの内容に詳しいレビュワーにまずレビューをお願いし、そちらのレビューがひと通り完了した段階でオーナーにレビューをお願いし最終確認をもらう、というパターンも多い。

たまに、ふだんあまりいじらないあたりのファイルをいじっていると、だれにレビューをお願いしたらいいのかわからないこともある。そのときは適当にログを調べて、最近そのファイルをいじってたりそのファイルの変更をレビューしている人をピックアップしたりする。比較的最近、Chromiumで使っているgit-clでは、チェンジの内容からレビュアーをサジェストする機能が導入された。experimentalだが、わりとよいサジェスチョンをしてくれることも多い気がする。

ところで、Chromiumではコーディングスタイルを定めていろいろ細々と決めている。ベースはGoogleのコーディングスタイルなどだが、さらに細々としたことも決めている。このあたり、人によっては意見もあるだろうけれど、コーディングスタイルを決めることには、このタイプのレビューと親和性があると思っている。

レビューでは、いろんな人が勝手にいろんなことを言う。細かく、どちらでもいいことであっても、いったんこうと決めておくことでレビューが進められる。たとえば、Foo* barであってFoo *barでない、などのスタイルは、本来はどっちでもいいが、いずれにせよどちらかに統一すべきものだ。だからすぱっと決めてしまえばいい。そうすることで、機械的に問題をチェックすることすら可能かもしれない。瑣末なことを決めてしまえば、本質的なポイントにフォーカスしたレビューができる……まぁ理想的には……と思っている。

goma / ninja
コードレビューの話はいささか本題から外れてた気がするので話を戻す。

ChromiumはほぼC++なので、ビルドの高速化は至上命題だ。gomaはChromiumで使われるクラウドコンパイラで、ローカルのファイルをデータセンターのインスタンスに送り、コンパイル結果を返す。コンパイル結果はキャッシュされている。大半の場合はファイルの変更はないから、gomaのキャッシュにヒットするとコンパイル時間は凄まじく高速化される。gomaはもちろん開発者の生活も幸せにしたが、実際にはBuildBotやtrybotでも使われており、これらのチェックの高速化に寄与している。

ninjaは平たく言えばmakeの代替物。いわゆるビルドシステムだ。ChromiumではビルドにはGYPというソフトウェアを使っている。GYPは.gypファイルを読み込むと各種のビルドシステム用のファイルを生成する。WindowsならVCのプロジェクト、MacならXcodeプロジェクト、LinuxならMakefileといった具合。GYPはninjaサポートを済ませており、簡単な設定でこれらのファイルの代わりに(あるいは加えて)ninjaファイルを生成する。

ninjaはプロジェクトの目的に高速であることを第一に掲げていて、実際makeより格段に高速である。なぜそんなに速いのか、実際のところはよく知らないのだけど……。

まとめ
Chromiumでの開発環境をひと通り解説した。

正直なことを言うと、少しも特別なことはしていないと思う。当たり前のことを当たり前にやっているだけだ。だから、こういう文章がどれぐらい参考になるのかはよくわからない。

また改めて書くまでもなく、こうした手法やツールは相互に依存し合っている。trybotみたいな仕組みが必要なのはプラットフォームが多岐に渡っているからだし、またコミット前レビューの仕組みとも関わっている。Rietveldはコミット前レビューが前提なため、他のレビュープロセスなプロダクトで導入すると面倒なことになるかもしれない。gomaが大きな意味を持つのは、開発言語が主にC++であることも大きい。

開発プロセスに正解はない、と思う。あるプロジェクトでうまく行っている、というのを聞きつけて、その一部だけ取り出してもうまくはいかないかもしれない。どちらでもいいが一方を選択している、ということもある。ChromiumでRietveldが使われているのはたぶんGuidoが作ったシステムだからという理由でしかなく、WebKitにRietveldを導入したい、という意見はあまり興味を持たれないらしい。WebKitはbugzilla上でコードレビューも行うため、issueと正確に一対一対応されるが、Rietveldはバグトラッカーではないため、issue管理は別にやる必要がある。Chromiumではissueの管理はcode.google.comで行い、Rietveld上でissueと関連付けられるようになっている。レビューツールとissueツールが別、というのはぼくにとっては自然だけど、そこはまぁ好き好きかもしれないし、ぼくがなにかに洗脳されているだけかもしれない。

ちょっと話がそれたが、それでもまぁ、こういう文章もある種のケーススタディとして何かの参考になるとうれしいかな、と思う。

images:



2012-07-17

ChromeOS SKKの開発: 開発環境編


このシリーズは今回で最終回の予定です。

ChromeOS向けの「IME API」を使ってSKKを作りました、という話のシリーズなのですが、こういうものを作るときに何が一番厄介かといって、やっぱりデバッグだろうと思うわけです。
IMEはデバッグをするのが非常に厄介なたぐいのソフトウェアです。うっかり変なことをすると完全に壊れて何もできなくなることすらあります。ChromeOSのIME APIでは、IMEは単なるChrome extensionなために完全にsandbox化された環境で動作しますし、developer toolsもあるのでデバッグは相当楽なほうですが、それでもなかなか面倒な面もあります。
面倒さの一つには、実機での動作確認が面倒だということが挙げられると思います。ChromeOSでは、まだセルフホスティングの開発環境が整っていません。普通にたとえばWindowsやMacでChrome extensionを開発するなら、適当にJSファイルを書いてunpacked extensionとしてロードして手元で動かして見ることができます。ですが、ChromeOSではローカルファイルを編集するためのテキストエディタがありませんし(と思ったら、そういう拡張機能もあるようですが)、なんだかんだでまだまだ大変な面が強いわけです。
そういうわけで、今回はMacで開発をしたのですが、じゃあどうすればいいのでしょうか。Macのほうで拡張機能のパッケージ化を行い、適当なサーバにアップロードして、ChromeOSのほうでダウンロードして……などとやれば出来ますが、毎度それをやるのはあまり実際的でもありません。

といった状況を踏まえ、まず開発をするための実行環境を作ってみました。それがソースコード中のtestpageというディレクトリです。
こいつのやることは簡単です。まずmock.jsというJSを読み込みます。こいつはchrome.input.imeというオブジェクトをグローバルに追加して、setCompositionとかclearCompositionとかのメソッドを登録します。この関数の実行結果は、適当にHTMLで画面に表示させます。一方でキー入力も適当なeditableな要素のほうで受け付けておき、onkeydown/onkeyupを使って適当なイベントリスナにキーイベントを横流しできるようにしておきます。
そうしておいて、のこりの拡張機能で使うJSファイルをロードすると、ある程度は動作するようになるという仕組みです。実際に、前回の記事で書いたAppEngineのホストにくっつけて公開もしています→こちら

前々回の記事で、AJAX-IMEのようにページ内に寄生するIMEっぽいJSの話に軽く触れました。今回はこれと似たアプローチで、ただし、変換エンジンの処理を直接書き下すのではなく、IME API互換のレイヤを間に挟んでいる、という解釈ができるかと思います。
この手法はなかなかうまく行きました。ちょっとコードを書いたら、すぐ実際にキー入力してみてどう表示されるのか確認できる環境がある、というのはいいものです。すぐ確認できることによる全体的な開発の高速化もありますし、実際に動いているのを目にすることができるというのは、とくに開発初期においてモチベーションを高めるという意味もあると思います。わりとすぐにそこそこのものが開発できたのも、この方式のおかげというのがあるかなあと個人的には考えています。

まあ、実際には、APIのドキュメントをちゃんと読まずに実装したので、いざ実機にロードしてみたらちゃんと動かなかった、みたいなよくある失敗もあるし、JSやイベント処理まわりの無理解のために動かなかった、みたいなしょぼい問題もたくさん踏んでいるので、あんまり自慢するようなポイントではないような気もします。また、現状ではonKeyEventの返り値を見ていないので、一部の機能は全然動いていないように見えます。それに、動かして確認するといった単純な手法よりは、細かいコーナーケースではユニットテストをちゃんと書くほうが大事だったりします……このコードではテストを書いてませんが、IMEはコーナーケースばっかりなので本当はテストが大事です。
なのですが、個人的には、テスト環境を実装してみたら割と簡単に実装できて、これが思いの外便利だった、というハックがちょっと面白かったので、紹介してみた次第です。


photo: http://www.flickr.com/photos/mjmyap/147909798/in/photostream/ by mjmyap

2012-07-11

SKK for ChromeOSの開発:辞書データの話


前回書いたように、ChromeOSで動作するSKKを開発しました。当初の予定を変更して、今回は辞書の話を書くことにします。

実は私は昔、AJAX-SKKというものを書いてみたことがあります。とはいえ、JavaScriptの練習用コードだったということもあっていろいろ不備があるわけですが、何といっても変換には、適当なcgiをでっち上げて変換サーバにしていました。今回は、そういうことはやめようと決意し、全部JavaScriptで動くことを目指しました。

SKKの辞書には、バリエーションがいくらかありますが、最大のL辞書は相当大きく、これをそのまま扱うのは簡単ではないように思えました。そこで当初の構想としては、処理にwebworkerを使うだとか、保存にはIndexedDBを使うだとか、そういう現代的なテクノロジーを活用することを目論んでいました。
目論んでいたんですが、結果的にはこういうのを使うのはやめてしまいました。実際に、そういうコードを書き初めていたのですが、完成前にふと思いついてものすごく単純な手法を試してみたところ、あっさり動作してしまったしパフォーマンス上もたいして問題ではないように見えたので、それで行くことにしました。
その手法というのは、SKKの辞書データを単純にJavaScriptオブジェクトに変換してしまい、全部オンメモリに持つ、というものです。また、JavaScriptオブジェクトはJSON.stringifyでJSONフォーマットにシリアライズし、FileSystem APIを使ってローカルなファイルに保存します。次回以降は、ローカルなファイルを読んでJSON.parseするだけで辞書データが復元できます。
SKK-JISYO.Lをダウンロードしてパーズと保存を全部組み合わせた場合、手元の最新のChromebook (Series-5)で試すと、数秒で処理が完了してしまいます(しかもほとんどの遅延は辞書のダウンロード時間な気がします)。JSON.parseでローカルから復旧する場合は1秒強、といったところでした。辞書データは、バックグラウンドページがロードされたとき(つまりログイン時)に一度だけロードすればよいので、この程度の性能なら特に問題ないように思います。ログイン時のもろもろの処理に紛れてしまうんじゃないでしょうか。
むかし、SCIM-SKKを開発したときは、SKK-JISYO.L全体をはじめにパーズすると無視できない遅延が考えられるため、いろいろ効率化を工夫した記憶があるのですが、ああいう細かい工夫はいったいなんだったのかなぁという思いの去来する出来事でした。最近のコンピュータも、最近のJSエンジンも、速いよね……。


さて、それなのにソースコードには「server」の文字が含まれます。これは何をしているのかというと、辞書ファイルのミラーリングと、簡単な前処理です。
SKKは歴史があるので、辞書の配信もいささか時代がかった方式になっています。ファイル名自体が.gzの拡張子を持っていて、Content-Type: x-gzipのレスポンスヘッダがあります。コンテント自体を圧縮して配信するなら、Content-Encodingを使うのがHTTP的には正しかろう、とは思いますがブラウザのバグだとか歴史的な事情を考えると、この方式はむしろ自然だと言えるでしょう。ただ、自然だといっても、このままではJavaScriptで.gzを伸長するはめになります。これは今ひとつ現実的ではない気がします。
それに、SKK辞書はEUC-JPでエンコードされているという問題があります。.gzを伸長しても得られるのはEUC文字列ですから、Unicodeに変換しなければならない。これをJSでやるのはさすがにアホくさい。
そういった事情があり、AppEngine側でSKKの辞書のミラーリングをすると同時に、もうちょっと楽に扱えるように前処理を施します。
AppEngineではscheduled taskで1日に1回、openlabの辞書の配信元に問い合わせます(現在はopenlabが停止しているのでスケジュールも止めています)。で、更新のあった辞書ファイルをダウンロードしてきたらzlibで伸長し、そのデータをblobstoreに保存します。
拡張機能のほうからデータを持ってくるときには、単にサーバに辞書ファイルを問い合わせます。すると、blobstoreに保存してあるデータをContent-Type: text/plain; charset=euc-jpで返します。文字コードの変換はChromeがやってくれるので、JSレベルでは気づかぬうちに扱いやすい普通のテキスト形式でデータが手に入る、というわけです。

ちなみに、辞書ファイルは誰でも取ってこれます。たとえば http://skk-dict-mirror.appspot.com/SKK-JISYO.S.gz にアクセスしてみてください。openlabが停止している今では、偶然ですがキャッシュとして使えるかもしれません。


ところで、この話を会社でしたところ、そんなものはXHRでなんとかなるのではないかという指摘を受けました。あんまり詳しくなかったのですが、XHRでは返ってきたレスポンスのContent-Typeを上書きすることはできます。なのでContent-Typeだけならそれでもいいのですが、今回の場合はContent-Encoding: gzipも足してあげないとChromeはレスポンスボディを解釈できません。XHRではほかのレスポンスヘッダはいじることができないようなので、これだけでは無理なのではないかと思いました。ただ、Chrome拡張機能のwebRequest APIを使えばレスポンスヘッダをいじれるため、組み合わせればAppEngineがなくても良かったかもしれません。気づいてからずっとopenlabが落ちているのでまだ試していないのですが、復旧したら試してみるのもいいかもしれませんね。

image: http://www.flickr.com/photos/62396887@N00/1405476175/

2012-07-08

ChromeOSで動作するSKKを作った

ChromeOSには標準でいろんな言語がサポートされています。言語サポートとひとくちにいってもいろいろあるわけですが、入力方法もそうしたサポートのひとつでしょう。日本語にはMozcが使われており、中国語や韓国語も各種のOSSライブラリが使われています。CJK以外のアジア諸語ではlibm17nが使われています。ラテン文字を使った言語についても、様々なキーボードレイアウトをサポートすることで入力に対処しています。
とはいっても、そういうのは全然完全じゃないわけです。Mozcだけでは日本語はサポートしきれません。中国語でも、主として本土の人向けのピンイン入力や、台湾でよく使われるzhuyin入力や、Canjie(部首変換)はサポートされていますが、boshiamyのようなプロプラエタリなインプットメソッドは導入できません。
そういったわけで、ChromeOS用にインプットメソッド拡張機能APIというものが提案されており、これが現在、バージョン21(devチャンネル)でのみ利用できる状態になっています。このAPIを使えば、たんにJavaScriptで拡張機能を書くだけでインプットメソッドを導入することができます。
というわけで、昔とった杵柄、という塩梅でSKKを書いてみた、というのがこちらになります。ソースコードはgithubに公開しています。そういえばライセンスとかreadmeとか、ちゃんと書かないとな……。
折角なので、苦労話とか実装の話を適当に何回かにわけて書こうかと思います。ところでSKKといえば、openlab.jpがもうかれこれ2週間以上、ストップしているように思われ、何があったのか不安な面が強いのですが、辞書については諸般の事情から、てきとうなappengineインスタンスを作ってそちらでサーブしていますので、この拡張機能の辞書のダウンロードについては心配ありません。

さて、手始めにIME拡張機能とはいったいなんなのか、どう動くのか、という話を書いてみたいと思います。
はじめに、おそらく誤解をしている人も多いと思うので書いておきましょう。IME拡張機能は、よくあるAJAX-IMEのブックマークレットなどのようなものとは根本的に異なります。どちらが良い悪いというのではないです。ブックマークレットだけでブラウザ内で動くというのはとても優れたハックだし、すごい話だなと思うのです。ですがその場合、インプットメソッドはウェブのコンテントエリア内でしか動きません。
ChromeOSの場合はnothing but webですから、ウェブ以外に何があるのだろうと疑問に思うかもしれませんが、実際にはたくさんの入力エリアがあります。たとえばomniboxや検索ボックス。ネットワーク設定用のダイアログ。アプリケーションリストのポップアップ。などなど、実は意外とあるのですね。また、候補ウィンドウの描画などを、ホストするウェブページと独立にうまく描画するのは、そう単純な話でもないのです。
ChromeOSでは、内部的には現在、ibusというIMEフレームワークが使われています。ibusはマルチプロセスなIPCベースのフレームワークで、各IMEの入力エンジンはibus-daemonと別なプロセスで動作します。Chromeでキーイベントが発生すると、まずはibus-daemonを介して入力エンジンへキーイベントが届けられ、処理結果がまたIPCメッセージとしてChromeに戻ってくるという構造になっています。ちなみにChromeOSでは候補ウィンドウはibusではなくChrome自身が描画するという実装になっています。
図にするとこんな感じかな。
表現としてはわかりづらいですが、composition(未確定文字列)とかcandidates(変換候補)はブラウザプロセス内で表示/描画されます。
一方、AJAX-IMEブックマークレットなどの構成では、ウェブページ内にIMEが寄生します。てことで、ざっくり書くとこんな感じ?
表記上、わけられていますが、レンダラプロセス内にあるJSエンジン(v8)のなかにIMEの処理がロードされ、ブラウザプロセスから届けられたキーイベントを横取りして処理するというイメージ。この場合、すべての処理はレンダラプロセス内にあり、compositionやcandidatesの描画もレンダラを使って行ないます。htmlを使って自由に描画できるという面ではいいのかもしれないけど、ホストとなるウェブページの描画事情に応じて様々な厄介な問題が引き起こされがちです。あともちろん、omniboxなどウェブページの外側にあるモノにはアクセスできません。
IME-APIでは拡張機能を使うので、やはりIMEの処理がレンダラプロセスの中にロードされます。と書くと後者と似ているように聞こえるかもしれませんが、やはり異なります。なぜかというと、IMEの処理がロードされるレンダラプロセスは、ユーザがいま見て入力しようとしているウェブページのものとは異なるものだからです。
Chromeの拡張機能には「バックグラウンドページ」というものがいます。これは、その拡張機能が有効になっているあいだは、ずっと起動されているレンダラプロセスで、様々な処理を受け持つことができます(ちなみについ最近、こないだのGoogle I/Oにて、バックグラウンドページも必要なあいだだけ起動してあとは止めておく、という機能が紹介されています)。IME-APIを使った拡張機能でも、このバックグラウンドページにIMEの処理を持たせることが想定されています。この場合、バックグラウンドページのコンテンツというのは存在せず、ユーザが実際に目にすることはありません。どちらかといえば、さまざまな拡張機能APIにアクセスするJSコードのデーモン的な位置付けになっていると言えると思います。
ここに来ると、IME-APIというのは既存のibusに相当するレイヤとみなすことができます。ibusはd-busをベースにしたIPCフレームワークを使っていますが、IME-APIではChromeがブラウザプロセスとレンダラプロセスのあいだで行われるIPCレイヤによってibusと同じようなことを代替するという試みとなります。
このため、compositionやcandidatesの描画は、ibusと同等にブラウザプロセス内で動作します。ブラウザから見た場合、ある変換エンジンが拡張機能を使ってJavaScriptで書かれているのか、それともibusクライアントになっているのか、というのは大きな差がないようになっているわけです。

実装上はどうなっているかというと、バックグラウンドページは、基本的にはいくつかの初期化処理をしたら後はなんにもしない状態にしておきます。初期化のなかには、chrome.input.ime.onKeyEventというイベントハンドラにaddListenerによってハンドラ関数を登録します。そうするとユーザがキーを打鍵すると、そのたびに登録したハンドラ関数が呼ばれます。関数のなかでは、setCompositionやcommitText、candidatesの設定などの関数を呼ぶことができ、IMEの状態を任意に変えることができます。起動時のonActivateや、フォーカスの入出にかかわるonFocus/onBlurも使えば便利であろうと思われます。
そんな感じで、あとはキーイベントハンドラを上手く処理するJSコードを書きさえすれば完成という次第です。
次では、開発プロセスに関係することを書こうかと思っています。

2012-06-06

共産主義スーパーヒーロー! 『キャプテン・チャイナ』

今朝方、Google+でテクニカルライターのMike Elgan紹介していたコミックが凄そうだったのでうっかりKindleで買ってしまった。その名も『キャプテン・チャイナ』!!

……
amazon.comでKindle版が$2ぐらい。もっとも、この第1巻はエピソード1つ分で、20ページぐらいです。英語版と中国語版がバンドルされてて全体で40ページぐらい。

せっかくなので軽く中身を紹介しておきます。
……かつて、1950年代末、毛沢東は大躍進政策中にスーパーソルジャー計画を立案。数千人を越える兵士たちの候補から最終的に五人のエリートが選抜され、そのうちのとくに優秀な一人が中国の最初のスーパーヒーローに選ばれます。人民解放軍に由来するリベレーター(解放者)というコードネームを与えられ、当時の中国で大々的に宣伝が行われました(あ、解放者、というのは中国語版の名前)。ですが、当時の技術的な限界から、超人改造には無理があり、選抜者は全て死亡。大躍進政策自体も失敗に終わったことにより、この超人兵士計画は頓挫してしまいました。
それから50年。実は五人の候補のうちのひとり、リベレーターは死んでおらず、50年間冷凍冬眠していました。それを現代の技術によって蘇らせたのがキャプテン・チャイナ(中国隊長)、というわけです(なんでコードネームが変わったのかはよくわからないw)

さて、国際的な記者発表会でキャプテン・チャイナのお披露目をしたのですが、記者の財布を盗む、というお誂え向きのアクシデントが! 即座にキャプテン・チャイナは専用の武器、レボリューション・キャノン(革命砲)取り出し、泥棒を撃ち殺しちゃいました。
「犯罪はこの偉大な国では許されない!」
おかげでボスはカンカンです。泥棒の一幕はもちろんヤラセだったのでした。「パンチでも浴びせとけばいいんだ!」「ヤラセだと知らされていなかったので……」どうも部下(↑の後ろで立ってる金髪)が「その方が臨場感が出るから」と気を利かせて教えなかったのでした。「まあ、本物の犯罪者を使ったのは不幸中の幸いだったが……」そんなことでいいのか。おかげで革命砲には普段は弾を込めないこと、という縛りがつけられました。

さてそんな折、どこかで見たことのある黒人のアメリカ大統領が訪中することになりまして、キャプテン・チャイナ一行になぜか「アメリカ大統領が襲われる、守れるのはキャプテン・チャイナだけだ」という謎のタレコミがありました。どう考えてもその情報は怪しいし警備はしっかりしているから、というボスの至極もっともな意見に耳を傾けず、制止するボスの目をかいくぐってその場に行ってみると、飛行機を降りた大統領と中国の主席が握手をして挨拶をしているところを、小型の飛行ユニットで空をとぶヴィラン、ブラッディクロス(血十字)が襲撃している真っ最中でした。
ブラッディ・クロスさん
キャプテン・チャイナもさっそく戦闘に参加し、ほかの警備員のもっていた銃でなんとか応戦するもののいかんせん性能不足は否めません。「主席! この拳銃では非力です。レボリューション・キャノンの使用許可を!」「許可する!」といったわけでレボリューション・キャノンを使ってみごとにブラッディ・クロスを撃退。すんでのところで取り逃がしたものの、大統領の命は救われたのでした。めでたしめでたし。

とまあそんな感じ。
ほかにも、「コードネームはキャプテン・チャイナ(英語でcaptainは大尉のこと)だけど自分は軍では少佐だった」とか、どう拾っていいのかわからない小ネタがあったりして、マジなのかネタなのか判断に苦しんでいます。どうなんだろうこれ。
とはいえまだ最初のエピソードが終わったばかり。ブラッディクロスが何故かキャプテン・チャイナの本名を知っているという衝撃(まあいちおう……)の展開や、謎のタレコミをしたのが誰だったのかなど、伏線はいろいろある気がします。
ぼくはたぶん続刊は買わないと思いますけど。

2012-05-16

Go言語の型宣言をHaskellから理解する

という誰得記事を書いてみたいと思います。

Goの型システム……というかtype宣言はなかなか面白いんじゃないか、ということに最近気づきました。type宣言は新しい型を宣言するものです。たとえば、A Tour of Goのだと、
type Vertex struct {
X int
Y int
}
のように宣言します。これは当然のように思われるかもしれません。
ところが、type MyFloat float64 のように宣言することがあります。これはどういう意味があるのかというと、実態としてはfloat64なのだが新しい型として定義したい、という意思表示となります。

具体的に、これが意味するものとしてメソッド宣言を見てみましょう。VertexにAbsというメソッドを定義した場合:
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
このように表記します。このとき、*Vertex 型の変数 v について v.Abs() とメソッド呼び出しができるわけです。
同じように、type MyFloat float64 として宣言したMyFloatに対しても、メソッドの宣言ができます():
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
というか、どんな型に対してもメソッドの定義ができます。ただし、異なるパッケージの型に対して勝手にメソッドを追加したりできない、ビルトインの型も同様に挙動を変更させることはできない、という制限があります。そのため、既存の型と実態は同じだが独自の意味合いを持たせたい(実態としては数値だが特別な意味を持たせたい、というような場合)にこういう型定義によって別の型をつくることになります。この場合、Abs()というメソッドはMyFloatという型に対してついているのであって、float64に対して呼ぶことはできません。
つまり、float64MyFloatはほんとうに別な型なのです。たとえば上のコードサンプルでreturn fとすることはできません。Abs()が返すべきのはfloat64だからです。MyFloatfloat64は加減乗除等の演算も行えません。事前に型変換を行う必要があります。

たしか『入門OCaml』だったかとおもいますが、会計処理的なシステムで、消費税込みの金額とまだ税を組み入れてない金額が両方あるので混乱しがちになるという話が出てきて、phantom typeを導入して型安全を保つという事例があったとおもいます。同じことがGoのtype宣言では可能です。

この挙動は面白いし便利だと思う一方、C/C++のtypedefを知っていると、とても奇妙に思えます。C/C++のtypedefは簡単に言うと型に別名をつけるだけだからです。typedef float MyFloat、などと書いてもMyFloatfloatは区別されません。floatを引数に取るメソッドにMyFloatを与えても構いません。C/C++を使っている場合、これはこれで便利なものです。

ここでC/C++のtypedef相当の言語とGoのtype相当の言語を両方併せ持つ言語といえば、そう、皆さんご存知のHaskellですね(やっと出せた)。Haskellでいうとtype宣言がC/C++のtypedefであり、Goのtypeに相当するのはnewtype宣言だと言うことができるでしょう。
Haskellのnewtype宣言というのは、実態としては他の型と同等であるが、全く別の新しい型として定義したい、という場合に使います。たとえばA Gentle Introduction to Haskellでは、Integerと実態は同じであるが正数のみを扱うNatural型を定義しています(日本語訳)。Goのtypeはまさにこれと同じことをします。Natural型に対して定義したメソッドがIntegerに影響を与えないところも同じです。

ところで、Goにはtypedefはありません。既存の型に別名をつけることはできません。例えば上でVertexという構造体が出てきました。ここで、type Vertex2 Vertex などとしても、*Vertexに対して宣言されたAbs()メソッドは、*Vertex2型の変数にたいして呼ぶことはできません。
不便じゃないんでしょうか。そもそもtypedefってなぜ必要なんでしょうか。

Haskellのtype宣言は、既存の型に別名をつけるという意味で、まさにtypedefです。実例としても、String型は実際のところ文字のリスト[Char]型である、という代表例もあります。これはまさに同じであって、String型の値はリストとして扱うことができます。リストを引数に取る関数はすべてStringを扱うことができるし、パターンマッチもできます。これがnewtypeとして別の型になってしまうと、Haskellのリストを扱うためのパワーをスポイルしかねません。いっぽう、Stateモナドは、実態としてはただの関数だけども、newtype宣言でState型として宣言されています。こういうものはHaskellではtype宣言では扱いづらいし、ややこしい問題もありますが、newtype宣言によって状況はかなりクリアになります。
ようするに、「メソッドは引き継がれない」「別の型として扱われる」という点がメリットになるかデメリットになるか、というのがnewtypeを使うか、typeを使うかの違いになっているといえるでしょう。

Go言語では、どうして他の型のエイリアスを作らなくても問題にならないのでしょうか。たとえば、
var f1 MyFloat = MyFloat(1)
var f2 float64 = 1.0
f1 + f2
などとするとコンパイルエラーが発生します。f1f2は異なる型なので演算はできません。実際、上で挙げた例のように、税抜の金額型と税込の金額型を別の型として宣言している場合、この2つの型の変数同士を気軽に足せてしまったら分けた意味が薄れるので困る。f1 + MyFloat(f2)などとしなければならないわけです。

ところが、
var f1 MyFloat = MyFloat(1)
f1 + 1.0
これはエラーになりません。
Go言語には暗黙の型変換はないのだけど、数値リテラルは特定の型の値を持つわけではなく、文脈によって適切な数値型となり、実態として数値の型であるモノ(MyFloatとか)も数値型とみなされます。演算子もまた、数値型に対して有効であるという定義であり、オーバーロードや再定義ができません。このように演算子やリテラルを特別処理にすることで、暗黙の型変換なしに、異なる型であっても、こういうよくある表記では問題を起こさないようになっています。

また、メソッドについてはどうでしょうか。HaskellのString[Char]と同等なのはリストに対するオペレーションを使いたいからだと思います。ただ、Go言語のメソッドや関数では、多くの場合インタフェースが一致しているかどうかだけが大事で、継承関係を必ずしも持つ必要がありません。このため、既存の型と構文上も同等に扱いたいというモチベーションがそれほど高くないのではないか、と推測しています。
Go言語を設計するにあたり、どれぐらい慎重にこういったことが決められたのかはぼくは知らないのですが、こういった事例からは、「こうしておけば実用上問題ないでしょ」という思い切りのようなものがあるな、と思うのです。
実際のところ Haskell にしたって type 宣言を書くことはそう多くない気がします。なくてもかまわないといえばかまわない、Stringだけがうまくいけばそれで良い、そういうヤツなのではないでしょうか。だが、既存の言語の枠組みで「String[Char]のように扱う」ためには、type宣言のようなものを持ち出すか、さもなくばStringを特別扱いしなければなりません。特別扱いはイヤだ、言語の定義で直交性を保ちたい、という美的センスがあるのではないかと思うのです。

直交性というのはプログラミング言語の「美しさ」を語る上では大事なキーワードだと思うのですが、Go言語では、そこまで最重要なポイントとみなされていないんじゃないかという疑念を抱いています。もちろん軽視しているのではないでしょうが、「大事だけどほかに重視すべきことがある」という価値観があるように思います。
でもそれでいいじゃん、動くし実用上は問題ない、といえるのがGoの面白いところだなと思った、というところで締めたいと思います。