Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2012-02-21

unite-outline の近況: 次期バージョンの開発

昨年の秋頃から目立った変化のない unite-outline ですが、開発が停滞しているとか、作者のやる気がなくなった*1とかではなく、「今年の抱負 - Vimプラギン編」で述べた通り、次期バージョンの開発(準備)を少しずつ進めています。

ある程度形になったら別ブランチを切って push しようと思ってますが、まだ仕様をあれこれ考えたり非同期実行のやり方を模索している段階で、ものは全然できておりません(汗 まあ、上半期のうちに最初のバージョンを push できたらいいな、ぐらいの見込みです。*2

大きな変更が必要になるついでに、「もうこの際、たまった負債を全部返済してやる!」みたいな意気込みでして、プラグインの構成上前から気になっていた点や、作者がまずいと思っている outline info の仕様など、全面的に見直してドカンと刷新する予定です。(というか、そうせざるを得ないのです…… 詳細は以下を参照)

で、その次期バージョンですが、青写真は以下の通りです。

NuOutline(仮)

アウトライン解析部を独立したプラグイン(NuOutline(仮))として切り出し、unite-outline をそのフロントエンドとして添付するという構成をとります。

プラグインの構成上一番大きな変更点がこれです。

「unite.vim の source として unite-outline は大き過ぎる」、「アウトライン解析を行うプラグインに unite-outline を添付すべきでは?*3」という指摘は以前から何度かあり、自分でも気になっていました。また、

  • autoload/unite/sources/outline/ 以下のディレクトリ階層の深い位置にファイルがごちゃごちゃと多過ぎる。
  • その結果、オートロード関数の名前がやたら長くなる。
  • また、動作が複雑になってオプション変数も増えたが、unite.vim の source のそれはやはり名前が長くなる。
  • unite.vim の source とアウトライン解析部が渾然一体、なんだかなー

といったこともあり、アウトライン解析を行う部分を独立したプラグインとして切り出し、プラグイン全体の構成を仕切り直すことにしました。図で示すと以下のようになります。

f:id:h1mesuke:20120221221020p:image

きれいに 3層に分離しました。3者の役割分担は Webアプリケーションなどでおなじみの MVCパターンでとらえると理解しやすいです。アウトライン表示のインターフェースを提供してくれる unite.vim がビュー、見出し抽出のロジックを持っている NuOutline がモデル、両者の間に入ってあれこれと調停をし、見出しの自動更新のための面倒をみたりする unite-outline がコントローラというわけですね。

V, C は交換可能(にしたい)

NuOutline は(頑張って)unite.vim, unite-outline と密に結合させないで作ろうと思っています。unite-outline はあくまでフロントエンドのデフォルト実装として NuOutline に添付されているもの、という位置付けにして、第三者が別のフロントエンドを提供できる余地を残しておきます。(そんな人が現れるかは別にして ;-) )

が、リポジトリは unite-outline のままで!

どちらかというと unite-outline の方が NuOutline のオマケ的な存在なのですが、Vundle や neobundle.vim 全盛(?)の昨今、リポジトリURL が変わるというのは致命的だと思うので、開発は引き続き、現行の unite-outline のリポジトリで行います。まあ、使うだけなら NuOutline は unite-outline の影に隠れて見えませんし、ユーザーが使うのはあくまで unite.vim を通してですからね。

というわけでなんと、unite-outline という unite.vim の source に、プラグインがまるまる1個付いてくるということになります(笑)

オプション変数名が変化

「unite.vim上でどう表示するか」に関する設定を除き、オプション変数の所管が NuOutline の方へ移るので変数の名前が変わってしまいます。しばらくは古い変数の方も見るようにしたいところですが……

outline info → parser

unite.vim の source である unite-outline の source……的な存在であった outline info は所管が NuOutline の方へ移ります。それにともない名称も outline info から parser に変更し、配置場所も

  • autoload/nuoutline/parsers/

とします。*4unite.vim における sources, kinds, filters などと同様の位置付けになります。

DRY でない create_heading() よ消え去れ!

outline info の仕様には(今となっては)気に入らない部分がたくさんあり、その最たるものが見出し抽出のためのパターンが複雑になると create_heading() の中身が DRY でなくなるというものです。例えば、

'^\s*\(foo\|bar\|baz\|fizz\|buzz\)\>'

のようなパターンを設定したとして、マッチングが行われ、マッチが見つかって create_heading() が呼び出されたとします。その場合、マッチしたサブマッチがどれかによって処理を分けなければならないことがほとんどで、現行の outline info ではマッチした文字列に対して再度マッチングを試み、サブマッチのうちのどれに実際にマッチしたのかを判定するような DRY でないコードが氾濫しています。

これは outline info を書く側の問題ではなくて、そのようにしか書けなくしている outline info の仕様の方に問題があるのです。

「こんなことでは outline info を書いてみようと思ってくれた人も離れていくわ!」

と強く思った私は、outline info の所管が NuOutline へ移るこのタイミングで、その仕様を刷新することにしました。

まだ仕様を練っている段階ですが、

let s:parser.foo_pattern = "foo's pattern"
function! s:parser.create_foo_heading()
endfunction

let s:parser.bar_pattern = "bar's pattern"
function! s:parser.create_bar_heading()
endfunction

let s:parser.baz_pattern = "baz's pattern"
function! s:parser.create_baz_heading()
endfunction

こんな風に、見出しの種類ごとに個別にパターンを設定できるようにし、「どのパターンにマッチしたか」の判定までを本体側の責任として、適切なコールバックが呼び出されるようにします。parser側では見出し(辞書)の作成に専念できるようになるので、パターンマッチ方式の parser を作るための敷居はかなり下がるはずです。

構文解析が必要になるケースのサポート

create_heading() について力説した直後にアレですが、「正規表現によるパターンマッチ → create_heading() 」というやり方で見出しが拾えるファイルタイプは実は少なく、ほとんどのプログラミング言語では見出しを「ちゃんと」拾うのに構文解析が必要になります。

具体的に言うと、ctags などの外部の構文解析プログラムを呼び出し、その出力を解析して見出しの木を parser が自前で作る必要があります。

今後、対応ファイルタイプを増やしていくためにも、その辺の処理が書きやすくなるようなユーティリティコードの充実やドキュメントの整備などが必要と考えています。

まとめ

最初からちゃんと設計しとけや! ヽ(*`Д´)ノゴルァ

うう、当初は「正規表現でマッチさせて拾うだけやん」程度の軽いノリで作り始めたのでございます。最初にコミットされたバージョンの素朴さがすべてを物語っている……

しかし、まあ、ここまできたらプラグインとしてちゃんとしたものにしたいですね!

*1:むしろ逆で、変なブーストが入ったw

*2:今年の抱負ということになってるので、遅くとも年内にはw

*3:alignta に unite-alignta が同梱されているのがこの構成ですね。

*4:outline info は autoload/outline/ に配置してもよいことになっていましたが、これは autoload/unite/sources/outline/ が自作の outline info を配置する場所として位置が深すぎるだろうとの配慮からでした。通常、autoload/ に何かを配置させるにあたっては、プラグイン名できちんと名前空間を切るべきです。

2011-12-28

Vim script のベンチマーク

数万回くらい実行されるループの中で文字列が空かどうかを調べなければならないとする。

  • empty(str)
  • str == ""

さて、どちらがいいだろう? empty() を使う方が、文字列が空かどうかを調べる、という意図が明確になっていいような気がするが、empty() の方は関数呼び出しなので、ひょっとすると ==演算子に比べて相当遅いかも知れないぞ……

この手の疑問/迷いを解消するために、同じことをする複数の Vim script コード片の速度性能を簡単に比較できるツールを作ってみました。

+reltime と +float が必要。7.2以降の Vim なら多分大丈夫(だと思うけど自信はないw)

使い方はこんな感じ。最初の疑問を解消すべく、ベンチマークスクリプトを作ってみる。

let s:bm = benchmark#new("String is empty?")

let s:N = 10000
let s:str = "hello"

function! s:bm.empty()
  let i = 0
  while i < s:N
    if empty(s:str)
    endif
    let i += 1
  endwhile
endfunction

function! s:bm.op_equal()
  let i = 0
  while i < s:N
    if s:str == ""
    endif
    let i += 1
  endwhile
endfunction

function! s:bm.op_match()
  let i = 0
  while i < s:N
    if s:str =~ '^\s*$'
    endif
    let i += 1
  endwhile
endfunction

call s:bm.run(3)

で、:source % または :QuickRun すると……

Benchmark: String is empty?

Trial #1
  op_equal : 0.078297
  empty    : 0.086356
  op_match : 0.114139

Trial #2
  op_equal : 0.080296
  empty    : 0.086518
  op_match : 0.109003
    
Trial #3
  op_equal : 0.080530
  empty    : 0.091485
  op_match : 0.118427

こういう結果が得られます。

所要時間にはループのコストも含まれていて、実はそれが所要時間の半分以上を占めていたりするので*1、これがそのまま ==演算子と empty() の速度差というわけではないものの、whileループの中に置いた場合の差は大体この程度、というのがわかります。(空の whileループの所要時間も同時に計測すれば、ループのコストを差し引いたおおよその速度差も検証できるはず)

Vim script を書いていて、ある処理を記述する複数の書き方のどれを選択するかで迷ったとき、これを使って速度比較を行ってみると、思わぬ発見があるやも。


もっと詳細に性能の評価を行いたい場合は……

このスクリプトのように、ループの中身が1行だけみたいな場合、ループ自体のコストが無視できないレベルになるので、このツールは対象コードの純粋な性能評価には向いていません。どちらかというと複数ある選択肢の中のどれが一番いいか、を比較するためのものです。

もっと詳細に行単位で性能を評価したいとか、全体のボトルネックを突き止めたいとかいった場合には、Vim のプロファイル機能を利用するべきでしょう。

*1Vim script の while は for に比べて相当遅いです。付属のサンプル参照。

2011-12-05

word の中の単語を選択する textobj-wiw を書いた

(この記事は Vim Advent Calendar 2011 5日目の記事です。前日は thincaさんでした!)


前々から、地味にフラストレーションを感じていたこととして、

この状態から、"word" の部分を書き換えたい場合、 

this_is_a_word_in_a_very_long_identifier

 ↓

this_is_a_word_in_a_very_long_identifier

こう選択する text-object が欲しいんだよ! というのがありました。


また、選択と同様に、

ここから、

this_is_a_word_in_a_very_long_identifier

 ↓

this_is_a_word_in_a_very_long_identifier

this_is_a_word_in_a_very_long_identifier

this_is_a_word_in_a_very_long_identifier

こんな具合にジャンプしていきたいんだよ! とか。


で、この手の「word の中の単語」は上に挙げたような snake case の中だけでなく、thisIsAWordInAVeryLongIdentifier のような camel case の中にもあるし、this#is#a#word#in#a#very#long#identifier のような、Vim script のオートロード関数の名前の中にもあります。

それら「word の中の単語」を一様に取り扱える text-object があれば便利だと思い、kanaさんの textobj-user を使って textobj-wiw というのを書いてみました。

wiw は word in word の略です。「word の中の単語」とは要するに、「Vim における word に含まれている、人間が単語と認識している部分」のことです。

これを使うと、「word の中の単語」間をジャンプで移動できたり、おなじみの iw, aw と同様の操作で「word の中の単語」をサクッと編集/削除/選択できるようになります。

インストール

プラグインをインストールするいつもの方法で

の2つをインストールして下さい。

説明になってないような気もしますが、重要なことは textobj-user が必要だということです。

キーマッピング

textobj-wiw には w, b, e, ge, iw, aw に相当する一連のキーマッピングがあり、デフォルトでは以下のキーマッピングを定義するようになっています。

        lhs     Modes   rhs
        ---------------------------------------
        ,w      nxo     <Plug>(textobj-wiw-n)
        ,b      nxo     <Plug>(textobj-wiw-p)
        ,e      nxo     <Plug>(textobj-wiw-N)
        ,ge     nxo     <Plug>(textobj-wiw-P)

        a,w     xo      <Plug>(textobj-wiw-a)
        i,w     xo      <Plug>(textobj-wiw-i)

対応する動作がわかりやすいように prefix を付ける方式にしました。prefix は g:textobj_wiw_default_key_mappings_prefix変数で設定できます。(デフォルトは ",")

デフォルトのキーマッピング体系が気に入らない場合は、vimrc にて g:textobj_wiw_no_default_key_mappings 変数を定義すればデフォルトキーマッピングの定義をキャンセルできます。その上で、各自でよさげなキーマッピングを定義して下さい。

似ている text-objects

キャメルケースについては既にやっている方がいました。あわわ。

また、ある特定の文字で囲まれた部分、を取り扱う text-object としては thincaさんの textobj-between もあります。こちらは選択範囲が word内に限定されません。

探せば他にもあるような気がしますが、恐いのでこれ以上は探しませんw


それでは皆さん、Happy Vimmer days!

2011-11-06

unite-outline の近況: Ruby と RSpec の見出し抽出を改善

今秋から Rails やってます。というわけで、unite-outline の Rubyサポートを強化中。まずは、Ruby用 outline info の改善から。

def 以外のメソッド定義も拾う

なんと、メソッドの定義は def しか拾ってなかったので、attr_accessor, attr_reader, attr_writer, alias などが今まで見出しになっていなかったという驚愕の事実。おいw

こいつらもメソッドの定義だから見出しになるべき、ということで、そのように修正。

Rails だと mattr_ とか cattr_ とかもあるよねー、ということでそっちにも対応。

正しい構造のツリー

Ruby(というか Rails界隈?)では protected, private なメソッドを public なものより一段字下げするという慣習で書かれているコードが少なからずあって、Ruby ではインデントの深さを元に見出しレベルを決めている関係上、一連の privateメソッドが直前の publicメソッドの小見出しになるということが起こっていた。(:help unite-outline-known-issues を参照)

で、これはちょっとかっこ悪いというんで、protected または private の後で字下げしている場合には、字下げされているメソッドを protected または private の下に束ねて、直前の publicメソッドの小見出しにならないようにした。

Before:

f:id:h1mesuke:20111106040844p:image

▲ privateメソッドが publicメソッドの小見出しになってしまっている。

 ↓

After:

f:id:h1mesuke:20111106040843p:image

▲ private を見出しにすることで回避

これで絞り込みの結果に無関係なメソッドが残ることはなくなった。

DSL対応

後、ftdetect で複合ファイルタイプの設定をしなくても DSL用の outline info へ処理を委譲できる仕組みを実装した。これにより、ファイルタイプが ruby のままでもファイル名が *_spec.rb なら RSpec用の見出し抽出を行う、みたいなことができるようになった。

f:id:h1mesuke:20111106040845p:image

▲ describe, it などが見出しになっているが、実はファイルタイプはただの ruby

具体的には、オートロード関数 path#to#outline#{filetype}#outline_info() にて、呼び出し側から渡される context の情報を元に、別のファイルタイプ名(例えば 'ruby/rspec' とか)を文字列として返すと、そっちへ処理が委譲される。(ファイルタイプ・リダイレクトと命名)

Ruby用の outline info ではこの仕組みを使い、ファイル名が *_spec.rb なら RSpec用の outline info へ処理をリダイレクトさせている。

この仕組みを使い、Ruby の主要な DSL に順次対応していこうと思っている。とりあえず次のターゲットは Rake あたり。

2011-09-24

unite-outline の近況: 空行の挿入を控えめにした

クラスのような大きなまとまりどうしの間には依然として空行を入れるが、単に見出しの種類が違うというだけでは間に空行を入れないことにした。

元々この機能は、ハイライトによる見出しの色分けがまだできなかったとき*1に考案したもの。C のソースから見出しを抽出したときに、見出しの一覧がフラットにびっしりと並び、何が関数で何がマクロで何が構造体なのやら見分けにくかったので、見出しの種類が異なるものどうしの間に空行を入れて視覚的にグループ分けしようしたのが始まり。

見出しの種類をハイライトで表現できるようになった今、過剰な空行の挿入がかえってうるさく感じられるようになったので、前述のとおり、クラスのような大きなまとまりどうしの間を除き、単に見出しの種類が違うというだけでは間に空行を入れないことにした↓


f:id:h1mesuke:20110924052046p:image


以下はこれまでの見出し一覧。空行が多い*2

f:id:h1mesuke:20110924035047p:image


ハイライトがなかった時は後者の方がみやすかったと思うが、今となって見出しの種類はハイライトで一目瞭然だし、前者が特に見にくいとも感じられないだろう。むしろ余計な空行が入らない分、空間の利用効率も上がってすっきりしたと思う。

*1:unite-outline に見出しのハイライト機能が実装されたのは先月のことで、実はまだ新しい機能。

*2:ちなみに、マクロどうしの間に空行が入って、マクロと関数の間に空行が入っていない場合があるのは、関数形式のマクロを関数と同じグループにカテゴライズしていたから。

2011-09-15

「ガベージコレクションのアルゴリズムと実装」を読んだ 〜実装編〜

ガベージコレクションのアルゴリズムと実装」を読了。前半の「アルゴリズム編」で GC の基本アルゴリズムを網羅的に解説し、後半の「実装編」では実際の処理系のソースを追いながら GC の実装を見てみるという構成で、非常に面白かった。

ガベージコレクションのアルゴリズムと実装

ガベージコレクションのアルゴリズムと実装

以下、読みながら思ったこと、メモしたことなどをまとめてみる。主に後半の「実装編」から。本書の中で解説されているバージョンは1年以上前のものなので、最新の実装にはあてはまらないかも。

Python

採用されているアルゴリズム:

  • 参照カウント
  • マークスイープGC(循環参照したゴミの回収用)

記述言語:

  • C

Python のエンドユーザーのレベルでは GC の恩恵でメモリについては何も考えなくていいのに、C のレベル(Python自体の実装のレベル)だと参照カウントを意識しつつのプログラミングになってプログラマの負担が結構大きいなという印象。プログラマがメモリ管理している感がありあり。

特に参照を受け渡す境界部分で、参照の「所有権」を「渡す」のか「貸す」のか「乗っ取る」のかという判断。多分、参照の所有権の扱いについては紳士協定というか、規約のようなものがあって、この場合は「渡し」て、あの場合は「貸す」、それ以外は「乗っ取る」んだ、みたいなことが決まっていて、どこかにちゃんと文書化されていると思うんだが、勝手がわからないと簡単にメモリをリークさせそうな気がする。

DalvikVM

採用されているアルゴリズム:

  • マークスイープGC
  • ビットマップマーキング

記述言語:

  • C

マークビットマップのビットでイテレートするのでマーク関数を再帰的に呼び出す必要がない点と、

オブジェクトビットマップとマークビットマップの差分を XOR で取って死んだオブジェクトをイテレートして回収する辺りは、なかなか巧妙な仕掛けになっていて面白いなと思った。

ただちょっと気になったのは、CLZ のような、先頭からのゼロビットの数を数えるための機械語命令の存在を前提としたアルゴリズムになっている点。

  • Android端末の CPU は ARM と決まっているのでこれでいい
  • この手の命令は大体どんなプロセッサにもあるのでこれでいい

ということなんだろうか。この辺りまで低レベルになってくると不勉強で知識が足りなくて困る。勉強しないと。*1

後、ちょっと気になったこととして、C のマクロ(複数行にわたる比較的大きなもの)が読み難かったというのがある。特に、引数以外に呼出側のローカル変数などが使われているもの。これをやられるとマクロを抽象化された手続きとして読めないので、慣れていないとちょっと戸惑う。

Rubinius

採用されているアルゴリズム:

  • 世代別GC
    • マイナー:コピーGC
    • メジャー1:ImmixGC
    • メジャー2:マークスイープGC
  • 正確な GC

記述言語:

  • C++

世代別GC を採用しており、使用するアルゴリズムとヒープ領域が多段構成。確保したいメモリのサイズによって割り当て時に使用するヒープを切り替えるなど、構造が複雑な印象。

が、ミューテータは ObjectMemory という抽象的なインターフェース(Facade)を介してメモリの割り当てと解放を行うので、使用する GCアルゴリズムについては関知する必要がない。よって、GC部分の実装をいじるのでなければ、記述言語である C++ のレベルでは GC の恩恵の元に開発が行える。その点、参照カウントの Python より開発者の(メモリ管理の)負担が小さい。

一方、正確な GC であることと、C の拡張ライブラリとの相性は悪く、拡張ライブラリからの参照をハンドラで別途管理するなどの苦心が見られる。(この辺は CRuby用に C で書かれた拡張ライブラリに対応したいという Rubinius特有の事情)

組み込みクラスのメンバ変数のアクセサ関数をプリプロセッサマクロで生成しているのが面白い。C/C++ でメタプログラミング的なことをしようとするとこうなるんだっていう驚きが。と同時に、このマクロで生成するセッターにライトバリアを仕込む辺り、うまいなと思った。

他にも一部のメソッドを Ruby によるテキスト処理でコード生成しているとのこと。

プリプロセスにおけるテキスト処理でコード生成、本当の意味で「メタ」プログラミングじゃねーか、という新鮮な驚きがあった。

本書で解説されているのはコピーGC の部分だけ。世代別GC の全容を把握するには実際のソースをさらに読んでみる必要がある。特に ImmixGC の実装と、大きなサイズのオブジェクトをマークスイープでどう扱っているのか見てみたい。

V8

採用されているアルゴリズム:

  • 世代別GC
    • マイナー:コピーGC
    • メジャー1:マークコンパクトGC
    • メジャー2:マークスイープGC
  • 正確な GC

記述言語:

  • C++

ヒープの構造が複雑でぱっと見で挫けそうになった(笑)

メモリをどう割り当てるかによってクラス階層の大元を分けるという構成。インスタンスをヒープに割り当てるものはすべて HeapObject を継承する、といった具合。これは、オブジェクトシステムの設計(クラス階層の構成など)とメモリ管理戦略の設計が不可分であるということであり、相当慎重に設計しないと後々いろいろ破綻をきたすと思う。

ハンドルスコープではコンストラクタ/デストラクタが呼び出される仕組みをうまく使ってコールスタックとハンドルスコープを同期させている。この辺もうまい。コンパイラが仕込んでくれるイベントフックというわけ。

フィールド制御。なんとハックなやり方。コンパイラ任せにできないとなるとこういう工夫が必要になるのか。コンパイラから制御を奪いとって自分でフィールドを配置するという。やはり言語処理系を作るともなれば、この手の逸脱行為は往々にして必要になってくるのか。

後、Rubinius 同様、やはりアクセサ関数はプリプロセッサマクロで生成していた。この辺は定石なのか。DRY原則重要がここにも。

本書で解説されているのはマークコンパクトGC の部分だけ。世代別GC の全容を把握するには実際のソースをさらに読んでみる必要がある。

EncodeForwardingAddresses() 以降はちょっと待てよと思った。そこまでやんのかと。ここで使われているテクニックのミソは要は相対アドレッシングで、アドレスをどこかに設定した基準からのオフセットとして表現すれば、アドレスの表現に必要なビット数を減らせるというもの。それ自体はへーなるほどなーって感じなんだけど……

そこまでしないといけないのかっ! という驚き。すべてのオブジェクトに 4バイトのフィールド、これが許容されない世界なんだなあと。ミューテータが生成するオブジェクトの数を考えたらまあ納得なんだけれども、まあなんとも泥臭い世界だ。

まとめ

「実装編」を読み終えて思うことは、やっぱり実際の処理系の GC は複雑で大掛かりで、実装上の工夫は総じて泥臭いということ。うまいなあ、なるほどなー、巧妙な仕掛けだなー、と思わされることはたくさんあるが、エレガントとか、美しいとか、そういうのとはちょっと方向性が違う。世代別ともなれば、ほんとにもう色々ごちゃごちゃしていて、本当に参ったという感じになる。

けれど、ソースコードを丹念に読んでいけば、ひとつひとつの処理は「探索」「マーク」「走査」「コピー」といったことに還元されていくわけで、GC は地道に愚直な処理をせっせとやっている、とても複雑で、巧妙な仕掛け、メカニズムだ。

それが動いて機能をなす、というのがなんとも面白い。

自分で GC を実装するのは、きっと面白いだろう。どんなアルゴリズムを採用しようかとか、ヒープの構成どうしようかとか、ちょっと考えただけでもなんだかワクワクしてくる。バグで生きているはずのオブジェクトを消し去ったりなんかした日には!

はやくそこまで行きたい。

*1:と言ってなかなかやらないわけですけど……

2011-02-26

デフォルトの outline info の作成ポリシー

昨夜、unite-outline の見出し一覧に表示される文字列を簡単にカスタマイズできるように、ユーザーが見出しの変換関数を既存の outline info にモンキーパッチできる仕組みを実装したところ、Lingr の Vim部屋にて、

「この機能は unite本体側に実装したほうがいいんじゃね」

という話になりました↓

自分としても機能の重複は望むところではないし、unite本体側の仕組みとして組み込まれた方がファイルタイプを横断して利用できるフィルタなども簡単に書けそうなので、昨夜の変更は早々に取り下げた*1のですが、

unite.vim の候補一覧の表示のカスタマイズは filter で!

という方向は確かなようですので、それを前提に unite-outline に同梱するデフォルトのoutline info の作成ポリシーを改めたいと思います。

デフォルト outline info の作成ポリシー(改)

情勢変化につき考え直し中

filter によるカスタマイズの余地を残しつつも、ctags を使うもの(C++, Java など)については tagbar の出力を手本にしようかななどと考えている。(2011-02-27)


デフォルト outline info の作成ポリシー

以下のようにします。

  • 見出し一覧の見た目には凝らない
  • なるべくバッファの行をそのまま(情報を欠落させずに)拾う

すなわち、デフォルトの outline info は「ユーザーは必要に応じて unite.vim の filter を書き、見出し一覧の表示をカスタマイズする」との前提に立ち、その入力として適当な、プレーンな見出しを作成するようにします。

ファイルタイプによっては*2デフォルトのままでは少々うるさい感じの表示になると思われます*3が、そこは「見出し一覧を見やすくする filter を書いて公開してくれる」多数のユーザーが現れることを期待し、こちらとしては「どう表示すれば見やすいか」という問題には関知しないことにします。

*1:partial_oinfoブランチに退避してあります。

*2:C++ とか Java とか、型の宣言がある言語は特に。

*3:というか、現にそうなっている。

2011-02-15

alignta の副産物たち

alignta って実は開発の過程でできた副産物の方がコードの量からいっても多かったりします。

その多くは人が既にやっていることで、目新しいものはないし、車輪の再発明もいいところなんですが、こういうものをこつこつ作ることで、結構 Vim script の経験値が上がりました。せっかくなので紹介してみます。(でもドキュメントとかないのです……すいません)

vim-oop

Vim script で OOP っていうのはちょっとググっただけでもいろんな人がやっているのですが、ご多分に漏れず、自分も挑戦してみました。目指したのは Ruby っぽい OOP で、クラスベース風味。

クラスベース風味、というのは、クラスベースの皮を被ったプロトタイプベースというほどの意味です。実際には個々のオブジェクト(辞書)が自身のメソッド(Funcref)を持っており、生成時にクラスオブジェクト(プロトタイプ)からコピーするという点がプロトタイプベースそのものなのですが、見かけ上クラスベースっぽく使えるので、そのように表現してみました。

見かけ上クラスベースっぽいというのは、インスタンスがクラスオブジェクトの単純なコピーではなく、クラスメソッドとインスタンスメソッドが峻別されているという点においてです。すなわち、クラスオブジェクトをレシーバとすればクラスメソッドが呼び出され、インスタンスをレシーバとすればインスタンスメソッドが呼び出されます。

また、(なんちゃって)クラス階層の概念があり、継承があります。*1 *2

alignta での使用例はこんな感じ*3です↓

let s:Aligner = alignta#oop#class#new('Aligner')

function! s:class_Aligner_apply_extending_options(options) dict
  let opts = (type(a:options) == type("") ? s:Aligner.parse_options(a:options) : a:options)
  call extend(s:Aligner.extending_options, opts, 'force')
endfunction
call s:Aligner.class_bind(s:SID, 'apply_extending_options')

function! s:class_Aligner_reset_extending_options() dict
  let s:Aligner.extending_options = {}
endfunction
call s:Aligner.class_bind(s:SID, 'reset_extending_options')

function! s:Aligner_initialize(region_args, align_args, use_regexp) dict
  let self.region = call('alignta#region#new', a:region_args)
  let self.region.had_indent_tab = 0
  let self.arguments = a:align_args
  let self.use_regexp = a:use_regexp
  " snip
endfunction
call s:Aligner.bind(s:SID, 'initialize')

function! s:Aligner_align() dict
  " snip
endfunction
call s:Aligner.bind(s:SID, 'align')

" snip

クラスメソッドの定義っぽいものと、インスタンスメソッドの定義っぽいものがあるのがわかると思います。

こんな風にインスタンスを生成して使います。

function! alignta#align(region_args, align_args, ...)
  let use_regexp = (a:0 ? a:1 : 0)
  let aligner = s:Aligner.new(a:region_args, a:align_args, use_regexp)
  call aligner.align()
endfunction

vim-unittest

Vim script のテスティングツールも、これまたいろんな人がやってるらしいのですが、ご多分に漏れず(以下略*4

Ruby の test/unit を参考にしています。個人的に RSpec より Shoulda派なので、テストを Shoulda っぽく書けるようにしています。

テストケースはこんな感じ*5です↓

let tc = unittest#testcase#new('test_base')

let s:Object = oop#class#get('Object')
let s:Class  = oop#class#get('Class')
let s:Module = oop#class#get('Module')

"-----------------------------------------------------------------------------

function! tc.Object_should_be_defined()
  call assert#_(oop#class#is_defined('Object'))
endfunction

function! tc.Class_should_be_defined()
  call assert#_(oop#class#is_defined('Class'))
endfunction

function! tc.Module_should_be_defined()
  call assert#_(oop#class#is_defined('Module'))
endfunction

" Object -(class)-> Class
function! tc.class_of_Object_should_be_Class()
  call assert#is(s:Class, s:Object.class)
endfunction

function! tc.Object_should_be_instance_of_Class()
  call assert#_(s:Object.is_instance_of(s:Class))
endfunction

" Class -(class)-> Class -(class)-> ...
function! tc.class_of_Class_should_be_Class()
  call assert#is(s:Class, s:Class.class)
endfunction

function! tc.Class_should_be_instance_of_Class()
  call assert#_(s:Class.is_instance_of(s:Class))
endfunction

function! tc.Class_should_behave_as_instance_of_Class()
  call assert#equal_C('Class', s:Class.name,   'name')
  call assert#equal_C('Class', s:Class.to_s(), 'to_s()')
endfunction

" snip

autoload/assert.vim が少し行儀が悪い感じですが、これはテストケースの字面優先でそうしました。*6

alignta ではこれを使ってテストを書いていて、正直これがないとやってられません。テストをちゃんと書いていたおかげで事前に発見できたバグも数知れず……

テスト重要ですよね!

以上

alignta の副産物たちでした。

*1:このクラス階層はサブクラスを定義する際にスーパークラスのメソッドをコピーするためだけ super() もどきを実現するためだけに使われるもので、メソッド呼び出しに際し実行時にメソッド探索が行われるわけではありません。(オブジェクトのメソッドは生成時にコピーされるので)

*2:また、スーパークラスのメソッドはクラスの定義時にコピーされるため、クラスの定義後にスーパークラスにメソッドが追加されてもサブクラスは追従できません。同様に、インスタンスの場合も生成時にクラスオブジェクトからメソッドをコピーするので、生成後のクラスオブジェクトの変更には追従できません。すなわち、クラスはオープンですが、クラスオブジェクトを実行時に変更した効果がサブクラスやインスタンスへは波及しません。

*3:適当に抜粋しています。

*4:なんというか、作りかけてから他の人がやってることに気付くというパターンが多い……

*5:上述の vim-oop のテストより。適当に抜粋しています。

*6:TestCase のインスタンスメソッドにすることもできたのですが、テスト関数内で call self. が乱舞して見苦しい感じなのでやめました。

2011-02-06

Vimノート - Vim script で構文解析するには、という考察

考察というか、ぼんやりと考えてみました的なもの。

まず、C/C++ の関数定義、あるいは Java のメソッド定義(コンストラクタ含む)を正規表現マッチによって拾うのはかなり苦しい*1。これらの言語の outline info は結構涙ぐましいものになっていて、「正規表現では括弧の数を数えられない*2」という至言とともに、「構文解析できたらなあ!」と痛感させられる。*3

Vim script正規表現でできるところまでやってやろうとは思うものの、Vim script構文解析という手法が使えるとしたら、どんな方法があるだろうか、と考え(妄想し)ておくことは、悪くないと思う。もしかしたら、ということもある。

まあ、半分ネタですけどw

1、ファイルタイプごとに構文解析器を Vim script で書く

Vim script で再帰降下な構文解析器をごりごりと書く。見出しとして拾いたいものだけ認識できればいいので、構文のサブセットを認識できる最小のもの、ということなら、頑張ればなんとか……なる?

問題は、言語ごとに構文解析器をスクラッチするみたいな感じになるので、あまり数はこなせないだろうなあということ。後、Vim script で字句解析やったら絶対ボトルネックになると思われ。

2、構文解析器の生成系を Vim script で書く

個々の言語用に構文解析器を書くのではなく、構文解析器の生成系を Vim script で書き、そいつが生成した構文解析器を使うという大胆不敵な戦術。Vim script版 yacc、すなわち vacc(または vison)を開発する。

問題は、作るのが難しそう*4な点w 後、Vim script での字句解析がボトルネックになるという点は 1、と同じ。

3、言語インターフェースを利用する

Perl、Python、Ruby、いずれの言語も構文解析器の生成系を持っているので、それらで生成した構文解析器を言語インターフェース経由で呼び出すという方式。1、2、に比べるとかなり現実的。生成系に食わせる文法ファイルを用意すればいいだけだし、そういうのは多分言語の総本山にいけば入手できるだろう。

問題は、ユーザーの Vim にその言語のインターフェースがあるかどうかわからないという点。ないと見出し抽出できませんではやっぱりダメだと思われる。なので、その場合は既存の Vim script正規表現方式に fallback する必要があるだろう。


とまあ、ざっと思いついたのはこんなところ。他にも ! を使って system() を使って外部コマンドを呼び出す方法や libcall() を使う方法なども考えられる。どちらにしても、「Vim script だけで」っていうのは難しい感じ。

また何か思いついたら書く。

*1:目印となる予約語がない、というのが一番大きい。予約語がないと、構文上こうなってるよね、というのを正規表現で書かないといけないのだけど、それがかなり難しい。大体は、ゆるめにマッチさせて、やっぱり違った、というものは Vim script ではじく、みたいな形になる。

*2:文脈自由文法 ⊃ 正規文法

*3:一方、Ruby や Python みたいな LL系の言語は大抵見出し行に class とか def みたいな予約語があるので楽ちん♪

*4:世の中、難しそうに見えるものが本当に難しいかどうかはわからないもので、こういうのも偏見なのだろうなあとは思います。

2011-01-31

unite-outline まとめ

unite-outline の関連エントリが増えてきたのでまとめ。自演乙。*1

*1:ちなみに、私は長島☆自演乙☆雄一郎選手のファンです ( ̄ー ̄)bグッ!