Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

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-09-18

unite.vim の file_rec でプロジェクトのファイルを一望する

unite.vim 使ってる人はみんな似たことやってるんじゃないかと思いつつ……

現在編集中のファイルが所属するプロジェクトのトップディレクトリ*1を起点に unite.vim で file_rec する設定。([unite] は unite.vim に割り当てている prefix)

これで、[unite]p とやると、プロジェクトのファイル一覧がずらずらと出てきます。後はキーを二三叩いて unite.vim おなじみの絞り込みを行えば、瞬時に目的のファイルを選択できます。

nnoremap <silent> [unite]p :<C-u>call <SID>unite_project('-start-insert')<CR>

function! s:unite_project(...)
  let opts = (a:0 ? join(a:000, ' ') : '')
  let dir = unite#util#path2project_directory(expand('%'))
  execute 'Unite' opts 'file_rec:' . dir
endfunction

これがあればはっきり言って project.vim なんぞいらないです。

*1:.git があったり Makefile があったり、configure があったりするディレクトリのことです。

2011-09-10

日本語プログラミング言語探訪 〜Mind〜

Mind

概要

日本語プログラミング言語 Mind


Mind はソースコードを日本語で記述するという特徴を持ちながら、大規模で信頼性が要求されるプロ仕様のアプリケーションが開発できるコンパイラ言語です。


単語名だけでなく文法まで、すべて「日本語」で記述できる高性能コンパイラ、それがMindです。日本語を使って、まるで日常会話のような感覚でプログラムを記述できるので、コメントに頼らない抜群のドキュメント性と高い開発効率が実現できます。

サンプル

公式サイトより。

 午前?とは                           ←「午前?」の定義 
    時刻を得て                            
    時が 12より 小さいこと。                   
                                     
メインとは                                
    午前?                    ←「午前?」の引用 
        ならば 「おはよう」を                  
        さもなければ                       
            「こんにちは」を                 
        つぎに                          
    表示し 改行すること。                         
半分とは 2で 割ること。                   ←「半分」の定義  
                                     
四分の一とは 半分の 半分。                  ←「半分」を引用して
                                 「四分の一」を定義
メインとは                                
        値1は 変数                       
        合否は 変数                       
    起動パラメータから 単語切り出し 値1と 合否に 入れ      
    合否が 偽?                           
        ならば 「数値の指定が誤りです。」で 重大エラー     
        つぎに                          
    値1の 四分の一を 数値表示し 改行すること。  ←「四分の一」実証
メモ
  • コンパイラ言語
    • 日本語であることを除けば普通のコンパイラとのこと
  • システム記述が可能
    • ビット操作、シフト演算、メモリの絶対番地アクセス、I/Oポートアクセスなど、低レベルな処理を記述可能
    • デバイスドライバの記述さえ可能とのこと
  • レストラン情報サイト「ぐるなび」の全文検索エンジン「MindSearchII」が Mind で開発されている。
    • 実用的な多くのアプリケーション開発実績
  • 分かち書きの必要あり

以下、資料[2] より。

  • 元になったのは Forth で、スタック指向
    • プログラミング中はスタックの使用状況を強く意識する必要がある。
  • 自然言語処理は基本的にやっていない
    • 分かち書きが必要
    • 文字種の違いをトークンの認識に積極的に使用
    • ひらがなの活用語尾を基本的に無視することで自然な表現を助ける。
  • 一部中置記法も可能(四則演算など)
資料
  1. プログラミング言語 Mind
  2. Mind - Wikipedia
  3. ソフトウェア評論 日本語プログラミング言語Mind

2011-09-07

unite-outline が見出しを自動更新するようになりました

やった! これで <C-l> ともおさらばだ!

unite-outline の「なんでこれができないの?」の筆頭であった見出しの自動更新をついに実装しました。詳細はヘルプで g:unite_source_outline_filetype_options、unite-outline-filetype-option-auto-update*1 の辺り見てもらいたいのですが、ここで簡単にその設定について解説してみます。

なお、以下の設定はファイルタイプごとに、g:unite_source_outline_filetype_options変数に設定します。とりあえずデフォルトを変更したい場合は「すべてのファイルタイプ」を意味する "*" の設定を変更すれば OK です。

auto_update

見出しを自動更新するかどうか。

1 に設定すると見出しの自動更新が有効になります。
0 に設定するとこれまで通り。見出しを更新したい場合は自分で <C-l> を押します。

デフォルト値は 1 です。

auto_update_event

見出しの自動更新の契機となるイベント、以下のどれか。

  • "write"
    バッファをファイルに書き込んだ時点で見出しが更新されます。通常はこれで十分です。
  • "hold"
    "write" のタイミングに加え、一定時間('updatetime' に設定された時間)カーソルを放置したタイミングでも見出しを更新します。-no-quit な場合に真価を発揮する少しアグレッシブな設定です。*2

デフォルト値は "write" です。

自動更新をオンにしていると unite.vim のウィンドウが開くタイミングでも更新がかかるので、実のところ見出しの一覧を -no-quit で表示させない限り "hold" を選択する意味はあまりありません。-no-quit で使うなら "hold" を使うかどうかを選択、そうでないなら "write" のままがいいでしょう。*3

ファイルタイプごとの特性を見極めて

ファイルタイプによって、見出しの抽出に要する時間や、扱うファイルの平均的なサイズも変化してくるので、そういったファイルタイプごとの特性を見極めた上で、

  1. 自動更新を使わない
  2. 自動更新を使う(更新契機は "write")
  3. 自動更新を使う(更新契機は "hold")

から最適なものを選択して下さい。

以下は、デフォルトでは "write"、C++ では自動更新なし、Markdown では "hold" に設定している例です。*4

        let g:unite_source_outline_filetype_options = {
              \ '*': {
              \   'auto_update': 1,
              \   'auto_update_event': 'write',
              \ },
              \ 'cpp': {
              \   'auto_update': 0,
              \   'ignore_types': ['enum', 'typedef', 'macro'],
              \ },
              \ 'javascript': {
              \   'ignore_types': ['comment'],
              \ },
              \ 'markdown': {
              \   'auto_update_event': 'hold',
              \ },
              \}
追記

Windows では外部プログラムの呼び出しに予想以上に時間がかかる*5ようで、見出し抽出に ctags を使っているファイルタイプ(C, C++, Java, Python)では自動更新にともなう待ち時間が許容できないレベルになることがあるようです。

そのような場合は残念ですが自動更新をオフにして下さい。

この問題を解決するためには外部プログラムの呼び出し部分を非同期にするなどの変更が必要になり、作者の勉強時間も含めてしばらく時間がかかると思われます……

*1:名前が長くてすいません(汗 名前が何かと長くなるのはプラグインプラグインの宿命です。

*2:'updatetime' のデフォルト値は 4000(4秒)でこのままだと少し待たないと更新がかからないので、1000 くらいに設定するといいかも知れません。

*3:-no-quit でない状況で "hold" を使っても、細かな更新作業が増えるだけで恩恵は一切受けられない(見出しの更新に連動して表示を更新させるはずのウィンドウが開いていない)ので、"write" の方が Vim にやさしいです。

*4:これは unite-outline のヘルプにある例のための例です。作者の推奨する設定ではありません。

*5:ちなみに作者は主に Vim を Ubuntu上で使っており、unite-outline の開発ももっぱら Ubuntu でやっているので、そういう問題に気付きにくいのです。

2011-08-21

alignta 秋の仕様改訂(案)

alignta の仕様が変わりました、というエントリではありません。

verion 0.2.0 から早半年。時間の経過とともに、現行の alignta の使いにくい点なども徐々に明らかに*1なってきました。そこで、こんな風に変えようと思ってるんですけどどうでしょう? という趣旨のエントリです。

問題の改訂案は、自分の中では大体この線で固まっているんですが、使っている人から「待った!」がかかるんなら、ちょっと立ち止まってみようかと。そんなわけで、作業に着手する前に、変更予定の項目についてまとめてみました。ご意見等ありましたらコメ欄にでも書き込んで下さると有難いです。

注意

このエントリの内容は随時書き換えます。また、必ずこの通りに仕様が変更されるということを保証するものでもありません。あくまで「案」です。(追記:2011-09-13)


以下の変更を行う予定です。

Vim の要求バージョンを 7.2 7.1 に上げる

Vim 7.0 を使っていた人から alignta を使おうとすると SEGV するという報告がありました。古い Vim でも動くようにしよう、という方向のモチベーションはあんまりないので、単純に 7.2以上を required にします。

とりあえず 7.1 を required としてみる。それでもやっぱりダメという報告があれば 7.2 にする。(追記:2011-09-13)*2

パターンは正規表現としてDWIM に解釈し、! はオワコン

現状、コマンド名に ! を付ける/付けないでパターンを正規表現として解釈するか否かを制御するようになっています。*3これは事実上 :Alignta と :Alignta! という2つのコマンドを使い分けているのに等しいので、「1つのコマンドですべてをまかなう」という要求に反しているように思います。

また、コマンドラインモードで alignta を使おうとして、

:Alignta \d\+

! 忘れた。ぐぬぬ……

という状況もいろんな人のところで起こっているようです。! の有無によってコマンドの引数の解釈を変える、という方法はあまり筋がよろしくない、というのが今となっての私の評価です。*4

よって、! の有無によってパターンの解釈方法を選択する、というのはやめ、パターンは原則、正規表現として解釈することにします。

! をなくす、という線は決まりだけど、パターンの解釈方法については試行錯誤中(追記:2011-09-13)

 ↓

エスケープしたいものを -e で指定する、正規表現として解釈したいものを -r で指定する、そのどちらを選択しても結局 -e または -r の指定忘れが発生し使い勝手に地味に響いてくると思ったので、

パターンを正規表現として解釈するか、エスケープして字面通りの文字列とするかをプログラムで判別することにした。(明らかに)正規表現のように見えるものは正規表現として解釈し、そうでないものは字面通りの文字列として解釈する。

この振る舞いを明示的に変更したい場合のみ -e または -r を前置する。

「正規表現のように見える」の定義についてはヘルプに記載する。ほとんどのケースで -e も -r も指定することなく、ユーザーの意図した解釈が行われればよしとする。(正規表現のようには見えないが実は正規表現、みたいな特殊なケースでは -r を指定してもらう)(追記:2011-09-14)


すべて正規表現として解釈するとなると、パターンによってはエスケープがめんどくさくなるものもありますが、そういうのは

 -e, -escape

を前置することで正規表現としての解釈を抑制できるようにしようかと考えています。

Vim の正規表現が苦手、という人にとっては現行のままの方がよい(安全)かも知れませんが、正規表現を一切使わないというわけにもいかないでしょうし、どうせ使うなら ! の有無で使い分けるより、終始一貫して正規表現である方がいいでしょう。

マージンの指定書式で @ を不要とする → 完了

マージンの指定と @ という記号の間には何の関係もない*5ので、この対応は覚えなければなりません。これは「覚えることを少なくする」という alignta の要求に反するのでなくします。

@ をなくすと直に数値で指定する形になりますが、

01
10
1:3
3:1

こういうのを整列のパターンにすることもそんなにないでしょうし、あったとしてもそこは数字じゃなくて \d が使われる局面じゃないかと思うので、実際のところパターンとの衝突は問題にならないのではと思います。

引数なしで実行した場合は前回と同じ整列を行う → ボツ

同じ条件(整列オプション&パターン)で整列を繰り返し実行したいことは多々あるので、こうした方が便利でしょう。

現在、引数なしで :Aligntaコマンドを実行した場合は g: or b:default_arguments変数に設定された引数リストを使うことになっていますが、これらの変数は deprecated とします。

当面は unite-alignta の候補として選択できるようにして、徐々に unite-alignta のバッファローカル設定への書き換えを促します。

〜な場合は前回と同じ整列を行う → 考え中

考え中

これは履歴機能の一部(特殊ケース)だなあ。(追記:2011-09-13)

後、整列の繰り返しを支援するため

  • repeat.vim に対応し、.(ドット)で繰り返せるようにする
  • 整列オプション&パターンの履歴をとり、unite-alignta から呼び出せるようにする

が ToDo となっています。


以上です。

ご意見等ありましたらコメ欄へどうぞ。Twitter へのリプも気軽にどうぞ。

*1:最初にリリースした時点で明察できるべきではありますが……

*2:ついこの間、循環参照にまつわる Vim の GC のバグ(?)を踏んだところなので、SEGV っていうとそういうところが怪しいのではないかと思うのだが、どこかに循環参照あったかなあ。vim-oop にはもう循環参照はないはずだし……ぬぬぬ。

*3:この仕様は、Vim の正規表現にあまり精通していないユーザーが、Perl互換の正規表現(以下 PCRE)とのメタ文字の扱いの違いで混乱するのを避けるためにこのようにしました。例えば、"(" や "{" の位置で揃えたい場合など、これらは PCRE ではエスケープしなければなりませんが、Vim の正規表現では逆で、エスケープすることでメタな意味を持つようになります。この辺のメタ文字の扱いについては、私自身 Vim を使い始めてからかなりの間馴染めず、ずっと eregex.vim のお世話になっていたので、「ユーザーはみんな、Vim の正規表現についてちゃんとわかってる」という前提には立たないことにしたのでした。そこで、「俺はわかっている!」という人は自分からその旨を ! で宣言してもらって、Vim の正規表現を思う存分使ってもらうと、そういう仕様でした。

*4:が、考えた当初はイケてると思っていたw

*5:一方、寄せを指定するための <, |, > などは直感的でわかりやすいと思っています。

2011-03-05

2つの文字列の編集距離を求める

unite-outline で使おうと思ったけど、ボツになったので貼っとく。

Wikipedia に載っていた擬似コードをほぼそのまま Vim script に落とし込んだだけです (^^;

レーベンシュタイン距離といわれるもので、2つの文字列がどの程度異なっているかを、文字列1を文字列2へと変形させるのに必要な手順の最小回数として求めます。帰納的なアルゴリズムになっていて、実装は意外にもシンプル。

参考

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-30

unite-outline が見出しとして抽出するコメントの形式と見出しレベル

前回のエントリで、unite-outline では、飾り枠線で装飾されたコメントを積極的に見出しとして拾うようにしていると書きました。*1

プログラミング言語用の outline info では、見出しとして抽出されるコメントの形式と設定される見出しレベルに一貫した決まりがあるので、今回はそれについて説明します。

といっても話は簡単で、outline info の "heading-1" なマッチにおいて、「線」と見なされるコメント行の下にある行が見出しとして抽出される、というだけです。そして、その見出しレベルは、マッチした「線」のパターンによって以下の表のように設定されます。

インデント線を構成する文字長さ*2見出しレベル
なし=41〜1
なし=11〜402
なし-
その他*3
41〜2
なし-
その他
11〜403
あり=, -
その他
11〜ファイルタイプによって 5 または 6 に固定*4
またはインデントの深さ + 3

コメント以外の見出しは見出しレベル 4以降からとなります。


この事実を利用して、例えば以下のようにコメントを付加すると、

"=============================================================================
" 見出し1

"-----------------------------------------------------------------------------
" 見出し2

"---------------------------------------
" 見出し3

function! s:foo()
  "---------------------------------------
  " 見出し5
endfunction

function! s:bar()
  "---------------------------------------
  " 見出し5
endfunction

見出し一覧は以下のようになります↓

f:id:h1mesuke:20110129234023p:image

これを積極的に利用することで、Vim script のような関数の定義がフラットに並ぶだけのような言語においても、その見出しを擬似的に階層表示させることができます。

追記

コメント以外の見出しがレベル 4以降からと聞いて、それではコメント見出しがない場合に、見出し一覧の全体が右に寄り過ぎるのではないかと思われたかも知れません。

unite-outline はその辺はうまく処理するようになっていて、例えば抽出された見出しのレベルが [4, 4, 5, 5, 4] の場合、それを [1, 1, 2, 2, 1] に変換してから表示します。

また、[1, 3, 3, 5, 7, 1] のような、中間レベルの見出しがない場合、見かけ上それをないものとして扱い、[1, 2, 2, 3, 4, 1] とします。

*1:実のところこれは unite-outline本体の機能というわけではなく、同梱しているデフォルトの outline info の実装上のポリシーに過ぎない。

*2:長さは大体の目安。桁位置ではないことに注意。

*3:C/C++ では */ も「線」を構成する文字と見なされるなど。

*4:インデントの深さがそのまま見出しレベルとなるような言語(Ruby や Python など)ではインデントの深さ + 3 となり、それ以外では 5 または 6 に固定される。Vim script では関数の見出しレベルが 4 であり、インデントされたコメント見出しはすなわち関数定義の中にあると見てそれより低い 5 となる。