Vim
vimscript

Vim script でクラスベースオブジェクト指向言語の継承っぽいことをする

Vim 8.0 からの Vim script が以前とは比べ物にならないほど便利なのでプラグインを書くのがはかどります。書く上でいい感じの書き方などを模索していて、いまクラスベースオブジェクト指向言語っぽくできないかといろいろ遊んでいます。具体的に言うとまあ、Python なんですけど、正直 Python も Vim script の100分の1も書いてないのであやふやかも。

まず、クラスはスクリプトローカルな辞書変数を作ってメソッドを生やすことで代替します。コンストラクタは同名のスクリプトローカル関数を定義することにしましょう。 Vim script では変数と関数は別のテーブルにのるらしいのでたぶん大丈夫。

" Foo クラス
let s:Foo = {
  \ 'key1': 'value1',
  \ }

" Foo クラスのメソッド
function! s:Foo.echo() abort
  echomsg 'foo'
endfunction

" Foo クラスのコンストラクタ
function! s:Foo() abort
  return deepcopy(s:Foo)
endfunction

次にこの Foo クラスを継承する Bar クラスを作ります。

" Bar クラス
let s:Bar = {
  \ 'key2': 'value2',
  \ }

" Bar クラスのメソッド
function! s:Bar.echo() abort
  echomsg 'bar'
endfunction

継承には専用の関数を用意します。

" 継承用の関数
function! s:inherit(subname, supername, args) abort
  let super = call('s:' . a:supername, a:args)
  let sub = deepcopy(s:[a:subname])
  call extend(sub, super, 'keep')

  let sub.__SUPER__ = {}
  for [key, l:Val] in items(super)
    if type(l:Val) == v:t_func || key ==# '__SUPER__'
      let sub.__SUPER__[key] = l:Val
    endif
  endfor
  return sub
endfunction

ちょっと読みにくい感じですが、親クラスのコンストラクタを呼び、子クラスのもととなる辞書のコピーに extend() しているだけです。親クラスのメソッドの関数参照は適当なところに保存しておきます。さて、この s:inherit() 関数を子クラスのコンストラクタで呼ぶことを継承と称することにしましょう。

" Bar クラスのコンストラクタ
function! s:Bar() abort
  return s:inherit('Bar', 'Foo', [])
endfunction

動かしてみます

let s:foo = s:Foo()
let s:bar = s:Bar()
call s:foo.echo() " foo
call s:bar.echo() " bar

いいかんじですね。さらに Bar クラスをつかってその親クラスのメソッドを呼びたい場合もあるかもしれません。これにも挑戦してみましょう。

" 親クラスのメソッドを呼ぶための関数
function! s:super(sub, ...) abort
  if !has_key(a:sub, '__SUPER__')
    return {}
  endif

  let level = max([get(a:000, 0, 1), 1])
  let supermethods = a:sub
  for i in range(level)
    let supermethods = supermethods.__SUPER__
  endfor

  let super = {}
  for [key, l:Val] in items(supermethods)
    if type(l:Val) == v:t_func
      let super[key] = function('s:supercall', [a:sub, l:Val])
    endif
  endfor
  return super
endfunction
function! s:supercall(sub, Funcref, ...) abort
  return call(a:Funcref, a:000, a:sub)
endfunction

この s:super() 関数を使うことで親クラスのメソッドを呼び出せます。

call s:super(s:bar).echo() " foo

たぶん親クラスの親クラスとかも行けるはず。

  • 書いたものの全体
" Foo クラス
let s:Foo = {
  \ 'key1': 'value1',
  \ }

" Foo クラスのメソッド
function! s:Foo.echo() abort
  echomsg 'foo'
endfunction

" Foo クラスのコンストラクタ
function! s:Foo() abort
  return deepcopy(s:Foo)
endfunction



" Bar クラス
let s:Bar = {
  \ 'key2': 'value2',
  \ }

" Bar クラスのメソッド
function! s:Bar.echo() abort
  echomsg 'bar'
endfunction

" Bar クラスのコンストラクタ
function! s:Bar() abort
  return s:inherit('Bar', 'Foo', [])
endfunction

" 継承用の関数
function! s:inherit(subname, supername, args) abort
  let super = call('s:' . a:supername, a:args)
  let sub = deepcopy(s:[a:subname])
  call extend(sub, super, 'keep')

  let sub.__SUPER__ = {}
  for [key, l:Val] in items(super)
    if type(l:Val) == v:t_func || key ==# '__SUPER__'
      let sub.__SUPER__[key] = l:Val
    endif
  endfor
  return sub
endfunction

" 親クラスのメソッドを呼ぶための関数
function! s:super(sub, ...) abort
  if !has_key(a:sub, '__SUPER__')
    return {}
  endif

  let level = max([get(a:000, 0, 1), 1])
  let supermethods = a:sub
  for i in range(level)
    let supermethods = supermethods.__SUPER__
  endfor

  let super = {}
  for [key, l:Val] in items(supermethods)
    if type(l:Val) == v:t_func
      let super[key] = function('s:supercall', [a:sub, l:Val])
    endif
  endfor
  return super
endfunction
function! s:supercall(sub, Funcref, ...) abort
  return call(a:Funcref, a:000, a:sub)
endfunction


let s:foo = s:Foo()
let s:bar = s:Bar()
call s:foo.echo() " foo
call s:bar.echo() " bar

call s:super(s:bar).echo() " foo

ファイルに保存して :source コマンドで読み込むと動きます。普段 matlab とか Fortran とかみたいな手続き型っぽい言語しか書かないのでなんか勘違いしてるかも。コメント欄でやんややんやしてください。