Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2011-02-15

alignta の副産物たち

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

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

vim-oop

Vim scriptOOP っていうのはちょっとググっただけでもいろんな人がやっているのですが、ご多分に漏れず、自分も挑戦してみました。目指したのは 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. が乱舞して見苦しい感じなのでやめました。