a thought of syntax hilighting

現在 wasavi のメジャーバージョンは 0.4 だ。これがいつ増加するかというと、おそらく

  • モデルとビューを分離したとき
  • シンタックス・ハイライティングを実装したとき
  • プラグイン・システムを実装したとき

のそれぞれになると思う。さてこの中でシンタックス・ハイライティングであるが、面白い論があったので勝手に訳出。意訳多数、訳質未保証。

* * *

シンタックス・ハイライティング論

君はソフトウェアを開発する際、シンタックス・ハイライティングに頼っているか? もし頼っているならば、もしかしたら君は自ら「墓穴を掘っている」かもしれない。このポストでは、美しく魅力的なシンタックス・ハイライティングについてその表現形式に焦点を移し、それがコードを見て理解しようとする人々の障壁になっていることを議論しよう。

背景
シンタックス・ハイライティングは、大半の現在的テキストエディタや開発環境では標準的な機能で、その基本概念は、プログラマがキーワード、記号、変数名を簡単に識別できるように、いろいろな構文的要素の見た目の違いを強調することだ。

そもそも、なぜシンタックス・ハイライティングが発明されたのか。プログラマは全てのシンタックスが織り交ぜられたコードを読む際、「”)” は変数名かな、もしかしたらプリプロセッサの命令かな?」などと考えるだろうか? もちろんそんなわけはない。コンピュータ・プログラムを読むことは大概難しいが、この難しさはそのプログラムの複雑さからくるものだ。シンタックスではない。

読みやすさ
シンタックス・ハイライティングはおそらく、コード読解の過程を加速するために発明された。イエス、その通り。強調されたコードは読みやすいに決まってる。その結果、強調されたコードは色を持つようになった!

うーん、ノーだ。タイポグラフィの基本的な経験則の 1 つに「まとまったテキストを書く際は、1 つの書体を選び、そしてそれを固守せよ」というものがある。は読者の注目を集めるかもしれないが、しかし必然的にテキストを読みにくいものにしてしまう。テキストの自然な流れは破壊される。破壊されたテキストは、個々の文字を繋ぎ合わせて単語と意味を得るためにより脳を酷使させることになる。認識という視点では、読解の過程から無意識の度合はがわずかに減少し、わずかに意識的になる。つまり、実際にテキストを理解する際の、心の意識的な部分の余裕を減少させる。

文法よりも意味が重要
コードを読む上で最も重要なのは、「理解」することだ。段落をざっと眺めれば、その要点を得られる小説や新聞と違い、ソフトウェアは複雑さで満たされており、細部にいたるまで全てが重要である。必然的に、理解するには時間がかかる。理解するためには、コードを意味のレベルで見る必要がある。

また、開発者は、よく知っているコードからさえ、単なるシンタックス・エラーよりもメモリーリークやセキュリティーホール、非効率なアルゴリズムを発見(そして修正)しようとするだろう。どの道コンパイラによって見つけられるのだから、開発者はシンタックス・エラーを探して時間を無駄にするべきではない。しかし、もしシンタックス・ハイライティングが開発者の思考を、コードが意味するものに対してではなくコードのシンタックスへと偏らせたならば、結局彼らは正にシンタックス・エラー探しを行ってしまうのではないか?

私は開発者達は愚かだと言っているのではない。私達は皆あちこちのいろいろなバグを見逃している──誤りは誰にでもあるものだ。せっかく作るのなら、ミスを助長するよりもミスの発見を助けてくれるツールを作るほうが良いと私は思う。

学習曲線
おそらく、シンタックス・ハイライティングは経験豊かなプログラマ向けのものではない。たぶんズレたピアノ教師が鍵盤に色つきのステッカーを貼るように、初心者にとっての学習曲線をなだらかにする意図でもたらされたのではないだろうか。ピアノ教師がそういうことをするのは、コミュニケーションを円滑にする(「さあ F を鳴らして!」より「さあ黄色い鍵盤を押して!」)ためなのかもしれない。彼らが本当に子供たちが音階名を覚えることなど無理だと思っているかは知らないが、子供たちは結局、音階名を覚えて、その後色つき鍵盤を忘れなければならない。

同じ現象がウィザードというユーザーインターフェースで見られる。高度な操作、例えば画像を扱うプログラムは複雑で、新しいユーザをまごつかせるが、ウィザードのダイアログボックスへ入力しさえすれば、柔軟性を犠牲にしていくつかのステップは省略あるいは自動化される。しかし結局、ユーザーは複雑な機能自体ではなく、ただ単にウィザードの使い方を覚えるだけで、そして特別な柔軟性は失われる。これは学習曲線をまったくなだらかにはしていない。学習していないのだ。

もし君が色のある環境でソフトウェアを書くことを学んだら、おそらく色なしの同じコード、あるいは異なるカラースキームのそれですら、理解にしにくい(あるいは、少なくとも不便だ)と感じるだろう。そう考えるとシンタックス・ハイライティングは教育の行き止まりだ。それは補助輪のついた自転車の乗り方を覚えるようなものだ。覚える過程で得た技術を捨て去るまで、普通の自転車には乗れないだろう。

例外
シンタックス・ハイライティングが実際に役立つケースが 2 つある。1 つめは複数行コメントに関連する。たとえば対話的な検索・置換操作の過程でソースコードファイル中を飛び回り、最後に巨大なコメントアウトブロックの真ん中で終了したら? 君はそれをコードだと思って読み始め、しばらくしてコメント終了のトークンにぶち当たる。そこで君はずっとコメントを読んでいたと気がつくわけだ。このケースでは、全コメントが異なる色で描画されることでミスは防げただろう。

しかしまあ、これは近視眼的な解決法だ。コードをコメントアウトするのは非常に一時的なデバッグ手法であって、コメントアウトされたコードは遅かれ早かれ削除か書き直さないとダメだということに多くの方々は同意する。視界から追い出すためにコメントアウトコードの色を変えるのは、まるでカーペットの下にゴミを隠すようなものなのだ。

2 つめは、主に C 言語に関するケースだ。たまたま「==」の代わりに「=」と書いてしまう、これは特に、長時間腰を据えてコードを凝視し、実際に目で見るまではなかなか発見しにくいバグになる場合がある。この状況のシンタックス・ハイライティングは、コードを意味論の文脈よりもシンタックスのレベルに焦点を当てることで有益になりうる、私が知る限り唯一のケースだ──それが「=」と「==」を異なる色で区別できるならば。やった! シンタックス・ハイライティングを実装するいい理由だ! だが……(おそらく君はここで驚いたりはしないと思うが)私が見つけた全てのカラースキームは、「=」と「==」を同じ色で強調している。

まとめ
シンタックス・ハイライティングはコードの読みやすさを改善せず、コードを理解させるのではなくざっと読ませるよう助長してしまう。シンタックス・ハイライティングは実際のバグではなくシンタックス・エラーに注目させ、それは学習の邪魔になる。おそらく、コメントアウトされたコード塊の削除をずるずると先延ばしさせたりもする。そして、現在の実装は「=」と「==」の区別(シンタックス・ハイライティングが有用だった唯一のケース)をしない。

一体誰がこんなひどい機能を発明したんだ? 推測するに、シンタックス・ハイライティングは実装して楽しいクールなアイデアとして始まった。今では、それはセールスポイントになった。人々は、例えそれ以外の機能が豊富だったとしても、シンタックス・ハイライティングをサポートしないエディタには眉をひそめる。これはたとえば半透明のコンソールとか、見ていて楽しいものについて共通する、十分に一般的な現象だ。

私は、例え苦くとも良薬を飲むよう勧める: シンタックス・ハイライティングなしでコードを書く、あるいはただ 2 色(コメントとコード)のみ使うという清貧的アプローチを受け入れる。警告: カラフルな見た目なしでは、君のコードはちょっと醜いかもしれない。しかし少なくとも、君が見ているものが現実のコードなのだ。

「この時、王様は裸でありません。王様はすばらしく色とりどりの道化師の服を着ていたのです」

handling wrapped rows #2

ということで、表示行単位でカーソルを上下に移動させるコマンド gj、gk を実装した。また、オプション jkdenotative を追加した。これがオンの状態では、単なる j、k が表示行単位で移動し、gj、gk は物理行単位で移動することになる。そして重要なことに、jkdenotative の初期値はオン、にしようと思う。

それにしても jkdenotative ってものすごく覚えにくいな。なんかいいのないかな。

さて vim では gj、gk のほかに g0、g^、g$ なんかもある。というかもっと他にやたらある。全部はいらないので、g^ と g$ くらいは作るかな。これらも jkdenotative の影響を受けることになる。とても重要。

handling wrapped rows

バッファ内において「物理行」を改行で区切られたそれぞれの領域とする。1 つの物理行を表示した際、ビューより長いものは折り返される。このとき折り返された、折り返しポイントで区切られたそれぞれの領域を「表示行」とする。

j コマンドや k コマンドは、物理行単位で移動する。ものすごく 1 行が長い物理行のうえでこれらのコマンドを実行すると一気にスクロールが行われて面食らう。

vim では、gj とか gk コマンドが用意されていて、これらは表示行単位でカーソルを上下に移動させる。vi にはない。

次はこれを持って来たいという話。

この機能を作る準備として、1 行内のその折り返しポイントを得ることを考えなければならない。できれば一発で取れる夢のような API があればうれしいのだが、ない。getClientRects() あたりが惜しいのだけど(これは各表示行についての座標情報を返す)、折り返しポイントは返してくれない。

うーん地道にループ回すしかないかな。

ところで vim で表示行を使う際、[cci]noremap j gj / noremap k gk[/cci] すると便利ですよ奥さん! 的な話題になりがちだと思う。でもこれは割とバッドノウハウなんじゃないのかなー。ぶっちゃけていうと j/k は常に表示行単位で動かすようにしても別に困らないと思うんだけど。移動単位を切り替えるオプションくらいは新設する必要があるかもしないが。

the heavenly school #4

LocationChange イベントのハンドラで、document に対して wasavi の起動と終了イベントをリスンさせているのだが、これはなかなかに乱暴だ。つまり、リスンさせっぱなしなのだ。

本来なら、タブが切り替わる直前に document からハンドラを取り除きたい。しかしそういうイベント(BeforeLocationChange みたいなの)はないようだ。LocationChange イベントは実質的に AfterLocationChanged だ。

したがってその結果、タブが切り替わるごとに同じイベントハンドラを何度も登録するというちょっと餡子の足りない動作になってしまっている。もちろん同一のイベントタイプ・ハンドラ・キャプチャフラグで何度 addEventListener を呼び出しても、登録は高々 1 度だけに収束するという DOM の仕様が前提なわけだけど。

Invoking addEventListener (or removeEventListener) repeatedly on the same EventTarget with the same values for the parameters type, listener, and useCapture has no effect. Doing so does not cause the event listener to be registered more than once and does not cause a change in the triggering order.

それと、現状では wasavi が終了すると共に [cci]passAllKeys = false;[/cci] しているのだけど、これだと wasavi に関係なく常にパススルーで動かしたいサイトで誤動作することになる。wasavi が起動する時点での passAllKeys の値を保存しておいて、wasavi 終了時はその値に復帰させるとかすればよいのだが、passAllKeys が実質グローバルなことを考えるとそれでつじつまが合うのか不安だ。いや合わないケースが確実にある。

この周辺をバグ報告されても、vimperator の仕様がそうなんだよーとおあしす運動(「おれじゃない」「あいつらじゃないの?」「しらない」「すんだこと」)を開始せざるを得ない。どうしよう。

the heavenly school #3

対象ページの window は、content オブジェクトが持っているみたいだ。なので、content.document instanceof HTMLDocument のときにそれに対してごにょごにょすればいい。

なお前の記事では MutationObserver を使うつもりでいたのだが、なにか使い方を間違っているのか、要素を追加・削除しても一切ハンドラが呼ばれない。しかたがないので MutationObserver を使うことはあきらめ、wasavi 側で開始・終了時に CustomEvent を document に対して発行するようにした。このイベントを vimperator のプラグイン側でリスンし、適宜 passAllKeys を書き換える。

ところで気になるのは、wasavi を適当なタブで起動し、そのまま他のタブに切り替え、また戻ってきた場合。この場合、LocationChange イベントのハンドラで、wasavi が起動しているかを見て passAllKeys を制御することになる。しかしここで passAllKeys を直接触らざるを得ないのは vimperator の間違った設計だと思う。LocationChange 内で passAllKeys を触るプラグインが複数あったら破綻してしまう。ハンドラ側では passAllKeys = true としたい、というリクエストを出すだけにして、ハンドラの呼び出しが全て完了した後に呼び出し側でリクエストの有無によって passAllKeys を制御する構造にならないかな。

まあそれはさておき、とりあえず編集したいときに wasavi を起動して終わったら即抜けるっていう使い方をする限りは動くようになった。やったね。

the heavenly school #2

そういうわけでいろいろ調べてみると:

  • 言うまでもなく、vimperator/pentadactyl は Firefox の UI を限りなく vim に近づけるエクステンションだ。ちなみにググった限りでは、vimperator は Firefox を「vim みたいな Firefox」にするのがゴールなのに対し、pentadactyl は「web ページを見られる vim」にするのがゴールのようだ
  • この手のエクステンションの宿命として、キーボード入力は誰のものかを常に意識する必要がある。web ページのスクリプトでハンドリングできる以上、本質的には web ページ上のキーボード入力はその web ページのものだ。vimperator のようなエクステンションは、web ページがキーボード入力を処理していない場合のみ成り立つ。wasavi はそれ自身が全てのキーボード入力を処理するので、当然両立し得ない
  • vimperator 側では、liberator.modules.modes.passAllKeys に true を与えることで、vimperator の動作をスルーさせることができる。また、vimperator にはプラグインの機構があり、例えばタブを切り替えたときに任意のスクリプトを走らせることができる。プラグインは ~/.vimperator/plugin(windows 環境では %HOME%\vimperator\plugin)の下に置く
  • wasavi がページ上に生成されたかどうかは、http://wasavi.appsweets.net/ をソースに指す iframe が存在するか、でとりあえず判断できる
  • つまりプラグインとしてのスクリプトで、DOM の変更を監視し、wasavi の iframe が生成されたら passAllKeys = true にするようにすればいいはずだ。

ということでそんな感じでプラグインを書いてみたのだが、うまく行かない。document.body 直下に iframe が追加・削除されたかどうか監視するために、MutationObserver を生成し、observe() する。
ところがこれが失敗する。MutationObserver#observe() するには対象の要素への参照が必要だ。つまりまず対象のページの window への参照が必要だ。そんなわけでこんな関数を:

function getWindow () {
var wnd;
try {
wnd = liberator.modules.Buffer.focusedWindow;
if (!wnd) {
log('target window not found.');
}
}
catch (e) {
log('exception occured while retrieving focusedWindow: ' + e.message);
wnd = null;
}
return wnd;
}

LocationChange に引っ掛けたハンドラ内で動かすと focusedWindow を参照したところで例外が起こる。vimperator 3.5 のソースを見てみると

getFocusedWindow: function (win) {
win = win || config.browser.contentWindow;
let elem = win.document.activeElement;
let doc;
while (doc = elem.contentDocument) {
elem = doc.activeElement;
}
return elem.ownerDocument.defaultView;
},

で、つまり elem が null のとき while の条件節でエラーになる。そりゃそうですね。

うわーん
動かないよー
うーんどうやって window を得たらいいのかな……。

the heavenly school

github で、vimperator や pentadactyl と共存させるとうまく行かないとの issue をもらった。そうなのか。

というわけでちょっと試してみることにしよう。しかし最初に告白をしておくと、これらのアドオンへの深い知識はまったく持ち合わせていない。というよりそもそも Firefox 自体のことをあんまりよく知らないんですけどね……。まあいろいろ調べながらやってみることにしよう。きっとどうにかなる。

タイトルには深い意味が込められているようであんまり込められてない。

a difficult AMO #2

例の AMO のご乱心だけど、特に直っている様子はない。

というか、ググってみたりツイって(謎)みたりしたり限りでは AMO にエクステンションをアップロードできないよー! 的なそれが見つからない。きわめて局所的な問題なのか?

bugzilla に登録されているバグ票では多少進展があったようだがまだなんとも言えない。休み明けもこの調子だったら追記するか。

 * * *

amo のエディタ(つまり編集者)にメールした。

あと、件の repack & compatibility-validation のメールが来ていて、そちらも「おめーのアドオンがエラーになってるから直せよな! 死ね!」という内容だったのだがエラーの内容はやはりサーバ側のタイムアウトなのである。どうしろというのか。

で、返信しようとしたらそのメールの reply-to: が nobody@mozilla.org だという。なんか……なんかなー!