bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

tlib#Object で vimscript OOP

VimM #1 and tlib

VimM #1 に参加してすっかり感想を書きそびれているのですが、非常に素晴らしくて、特にtaku-oさんの「ライブラリスクリプトを利用したvimエディタプラグインの構築」というお話が印象的でした。
#あと、id:kei-os2007 さんが話しかけてくれてすごく嬉しかったです!あのBrainF*ckのコードを1日2日で書くなんてすごすぎです。

ライブラリスクリプトを利用したvimエディタプラグインの構築(VimM#1)
http://nanasi.jp/articles/howto/note/vimm-200807.html

tlib#Input#ListW を用いて別windowを使うプラグインの書き方を解説されています。触発されて tlibのソースを読んでいたのですが、tlib の核となるtlib#World が使っている tlib#Object というオブジェクトシステムが面白いなと思ったので、使い方とかを調べてみました。

tlib の download は以下から。
tlib : Some utility functions - http://www.vim.org/scripts/script.php?script_id=1863

tlib#Object はtlib内部で使われるユーティリティで、prototypeベースの OOPっぽい書き方が出来るようになります。中身は普通の dictionary で、_class, _super 等の秘密の内部要素をコネコネして頑張ってる感じです。

ドキュメントとしては tlib#Object#New にしか解説が無いのですが、他のものに関しても(使われてないっぽいものもある)空気を読んで使ってみました。

サンプル

概略

  • ベースクラスの Person と、それを継承した Parent クラスがある
  • Parent は Person の hello() メソッドをoverrideしている
  • Parent には Personオブジェクトを格納するためのメソッド push_child() がある
" Person Class
" ----------------------------------------
let s:prototype_person = tlib#Object#New({
  \ '_class': ['Person'],
  \ 'name': "", 
  \ 'age': 0,
  \ })

function! Person(...)
  return s:prototype_person.New(a:0 >= 1 ? a:1 : {})
endfunction

function! s:prototype_person.hello()
  echo "My name is ". self.name
    \ ", age is " . self.age
  echo "  (object_id=" . self._id
    \ " class=" . join(self._class, ',') . ")"
endfunction

" Parent Class
" ----------------------------------------
let s:prototype_parent = tlib#Object#New({
  \ '_class': ['Parent'],
  \ 'children': [],
  \ })

function! Parent(...)
  " create Parent object and, inherit Person
  let object = s:prototype_parent.New(a:0 >= 1 ? a:1 : {}
    \ ).Inherit(s:prototype_person)
  return object
endfunction

function! s:prototype_parent.push_child(...)
  if a:0 == 0
    return
  endif
  for child in a:000 " a:000 is a @_ in Perl
    call add(self.children, child)
  endfor
endfunction

function! s:prototype_parent.hello() " override
  echo "My name is ". self.name
    \ ", and I have " . len(self.children) . " children."
endfunction

" test function
" ----------------------------------------
function! T()
  let bonar = Parent({'age':29, 'name': 'bonar'})
  call bonar.push_child(
    \ Person({'age':1, 'name': 'vimtaro'}),
    \ Person({'age':4, 'name': 'vimko'})
    \ )

  echo "parent info"
  if bonar.RespondTo('hello') " $bonar->can('hello') in Perl
    call bonar.hello()
    " call method in super class
    call bonar.Super('hello', [])
  endif

  echo "\nchildren info"
  for child in bonar.children
    call child.hello()
  endfor
endfunction

このソースをvimで開いて

:so %

した後、

:call T()

すると、以下のような出力が出ます。

parent info
My name is bonar , and I have 2 children.
My name is bonar , age is 29
  (object_id=3  class=object,Parent,Person)

children info
My name is vimtaro , age is 1
  (object_id=4  class=object,Person)
My name is vimko , age is 4
  (object_id=5  class=object,Person)
Press ENTER or type command to continue

tlib#Object overview

tlib#Object におけるオブジェクトは _class, _super という2つのListと _id という script global なオブジェクトカウンタを持った dictionary です。Inherit/Extend という2つの関数でオブジェクトを拡張させます。
# Inherit は本体で使われている形跡が無いですが。。

_class は自分が「関係している」のクラスを表現していて、単なる文字列のリストです。この値は IsA/IsRelated 等のメソッドで使用されます。Extendされるたびにこのリストに新しい文字列がpushされます。

_super は親となるtlib#Objectのリストで、Superメソッド(自分自身ではなく指定した親オブジェクトのメソッドを呼び出す)で使われます。

Extend() は自分自身を _super に設定し、与えられたオブジェクトの要素を自分自身に上書きし、自分の _class に与えられたオブジェクトのクラス名を(存在しなければ)追加します。Inherit() も似た挙動ですが、与えられたオブジェクトを _super に設定します。しかし継承しようとするオブジェクトと同じ名前の要素がある場合には上書きされません。

サンプルコードの説明

let s:prototype_person = tlib#Object#New({
  \ '_class': ['Person'],
  \ 'name': "", 
  \ 'age': 0,
  \ })

function! Person(...)
  return s:prototype_person.New(a:0 >= 1 ? a:1 : {})
endfunction

まずはプロトタイプとコンストラクタを作成します。Person() がコンストラクタです。tlib#Object#New は初期設定の dictionary を受け取った後 Extend() を使って受け取った値と _class, _super の設定を行います。

ちなみに、"a:0 >= 1 ? a:1 : {}" というのは良く出てくるイディオムで、Perl で書くと、((scalar keys %a) >= 1 ? \%a, {}) です。a:0 に引数配列の要素数が入っていて、1個目の要素が a:1, 2個目がa:2、引数配列全体がa:000 となっています。see also :help 41.8
#僕は最初戸惑ったのですが、vimscript 書く人には常識なのかもです。

他のクラスを継承する場合には、Parent() の様に Inherit() を使います。

function! Parent(...)
  " create Parent object and, inherit Person
  let object = s:prototype_parent.New(a:0 >= 1 ? a:1 : {}
    \ ).Inherit(s:prototype_person)
  return object
endfunction

Inherit() は extend() のkeepオプションで元のdictionaryを壊さないようにしているので、子クラス(この場合はParent)で指定された要素は残ります。サンプルではhello()が両方で定義されていますが、Parent.hello() が上書きされずに残る事になります。

サンプルの出力を見ても、call bonar.hello() で Parent.hello(), call bonar.Super('hello', []) でPerson.hello() が実行されているのがわかります。ただ、object.Super() は以下のように単純なループで指定されたメソッドを探していくだけなので、複数の親を持っている場合に望んだ動作を得るためには自分で書く必要がありそうです。

function! s:prototype.Super(method, arglist) dict "{{{3
    for o in self._super
        " TLogVAR o
        if o.RespondTo(a:method)
            " let self._tmp_method = o[a:method]
            " TLogVAR self._tmp_method
            " return call(self._tmp_method, a:arglist, self)
            return call(o[a:method], a:arglist, self)
        endif
    endfor
    echoerr 'tlib#Object: Does not respond to '. a:method .': '. string(self)
endf

メソッドの定義はプロトタイプのdictionaryに関数をセットするだけです。

function! s:prototype_person.hello()
  echo "My name is ". self.name
    \ ", age is " . self.age
  echo "  (object_id=" . self._id
    \ " class=" . join(self._class, ',') . ")"
endfunction

まとめ

prototype.js 等と比べると機能的に劣るものがあるかもしれませんが、vimscriptでOOP Like な書き方が出来るユーティリティで面白いですよね。みんなのvimscriptを見る目が変わると思います。tlibという1つのライブラリの中の機能としてはちょっともったいない気もしますね。(当然ながら)tlib が無いと使えないですし。。

tlib#World 等のtlibの鍵となるクラスもこの機構を用いて作られているため、これを足がかりにもうちょっと読み進めて行こうと思います。tlib はファイル数は多いものの1個のファイルが小さくてシンプルな印象です。

というかこういうフレームワーク的なものは実はもっとスタンダードなのがあって僕が知らないだけなんだろうなあ。。vim script はまだ右も左もわからないのでもっと凄い人のソースを読まねば。