この記事は Vim 8.0 Advent Calendar の 1 日目の記事です。
Vim 8.0 では Vim script の関数機能が強化されました。この記事では Partials とラムダを紹介します。
Partials
これまでの Vim script では function()
関数で関数参照(Funcref
)を作成できました。これにより、関数を変数に入れ、直接呼び出すことができます。
let Foo = function('strftime')
echo Foo('%Y-%m-%d')
" => 2016-12-01
echo Foo('%Y-%m-%d', 1482634800)
" => 2016-12-25
これに加え、function()
関数に事前に引数を渡すことで、引数部分をバインドした関数参照を作れるようになりました。これを Partial と呼びます。
let Foo = function('strftime', ['%Y-%m-%d'])
echo Foo()
" => 2016-12-01
echo Foo(1482634800)
" => 2016-12-25
function()
の第2引数以降に辞書を渡すことで、self をバインドすることも可能です。
function! Value() dict
return self.value
endfunction
let dict = {'value': 10}
let Foo = function('Value', dict)
echo Foo()
" => 10
辞書から辞書関数の値を参照すると、自動的に辞書をバインドした関数参照が得られます。
let dict = {'value': 20}
function! dict.get_value() dict
return self.value
endfunction
let Foo = dict.get_value
echo Foo()
" => 20
Partial から、バインドしている引数や辞書を得るには get()
を使います。
let dict = {'value': 20}
function! AddN(n) dict
return a:n + self.value
endfunction
let Add30 = function('AddN', [30], dict)
echo Add30()
" => 50
echo get(Add30, 'name')
" => AddN
echo string(get(Add30, 'func'))
" => function('AddN')
echo get(Add30, 'dict')
" => {'value': 20}
echo get(Add30, 'args')
" => [30]
ラムダ
これまでは関数を作るためには :function
Ex コマンドを使って、複数行に渡ってコードを書く必要がありました。
そのため、sort()
のような一部の関数を受け取る関数の実装が面倒でした。
function! MyCompare(i1, i2)
return a:i1 - a:i2
endfunction
echo sort([3, 5, 4, 1, 2], 'MyCompare')
" => [1, 2, 3, 4, 5]
そこで新しくラムダ構文が追加されました。{args -> expr}
という形式で、本文には式のみが書けます。
echo sort([3, 5, 4, 1, 2], { i1, i2 -> i1 - i2 })
" => [1, 2, 3, 4, 5]
また、map()
と filter()
は今まで文字列で式を渡していましたが、関数参照を渡せるようになりました。
これらの関数は、配列の添字の {index}
もしくは辞書のキー {key}
と、各要素の値である {val}
の 2 つを引数に取ります。間違いやすいので注意してください。
echo map(range(10), { index, val -> val * 2 })
" => [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
substitute()
関数も同様に {sub}
に関数を渡せるようになりました。渡した関数は、引数としてマッチした対象を配列で受け取ります。配列の 0 番目はマッチした対象の全体、1 番目以降はパターン内のグループです。
echo substitute('char count sample', '\w\+', { m -> m[0] . '(' . len(m[0]) . ')' }, 'g')
" => char(4) count(5) sample(6)
クロージャ
関数内で更に関数を定義した際に、クロージャを作れるようになりました。これはどういうことかと言うと、ネストした関数の内側から、外側の関数のローカルスコープを参照できるということです。
クロージャを使うには :function
Ex コマンドに closure
フラグを渡します。
function! Counter()
let c = 0
function! s:count() closure
let c += 1
return c
endfunction
return funcref('s:count')
endfunction
let C1 = Counter()
let C2 = Counter()
echo C1()
" => 1
echo C2()
" => 1
echo C1()
" => 2
echo C2()
" => 2
関数のスコープに注意してください。関数内であろうとも、関数自体のスコープは関数ローカルにはなりません。グローバル関数を定義すれば、それはグローバル関数になります。恐らくこれは歴史的な理由によるものだと思われます。
もしくはラムダを使うことでもクロージャを作成できます。ラムダを使う場合、ラムダ内の式が静的にパースされて、外側の変数を参照していた場合にクロージャと判定されます。よって、例えば :execute
や eval()
などで動的に参照されるだけの場合、クロージャにはならず外側の変数は参照できません。
function! OuterFunc(arg)
let var = 0
" これはクロージャになります。
let closure = { -> var }
" これはクロージャにはなりません。
let not_closure = { -> eval('var') }
" これは a:arg の参照によりクロージャになり、var も参照できます。
let closure = { -> [a:arg, eval('var')][-1] }
endfunction
2種類の関数参照
関数参照は function()
関数で生成できますが、これによって生成される関数参照は名前参照になります。これはつまり、関数が同名で再定義された場合、参照先の関数も新しい関数になってしまうということです。
一方で先ほどのクロージャは何度も再定義されることが多く、問題になる場合があります。そこで、関数が上書きされても元の値を参照し続ける新しい参照を作るために、funcref()
関数が追加されました。
funcref()
関数の引数は function()
関数と全く同じで、Partial も作成可能です。違いは、funcref()
呼び出し時点での関数自体への参照が得られる点です。これにより参照先の関数が後で上書きされても、元の関数を参照し続けることができます。
注意点として、組み込み関数に対しては使えません。組み込み関数の関数参照を作る場合には function()
を使う必要があります。