Vim script の closure を使うべきではない使い方
Vim の patch 7.4.2120 において :function
の引数に closure
というキーワードが追加された。
これは関数内から関数外の変数を参照するための機能である。
:func-closure
には次のような使い方の例が載っている。
function! Foo() let x = 0 function! Bar() closure let x += 1 return x endfunction return function('Bar') endfunction let F = Foo() echo F() " => 1 echo F() " => 2 echo F() " => 3
なるほど、確かに便利そうだ。
[問題点]
では、これの何が問題なのか。
まず、Vim script において『ローカル関数』というものは存在しない。
なので上記のコードのように『関数内で定義した関数』は『グローバル関数』(もしくはスクリプトローカル関数)として定義される。
つまりどういうことかというと『Foo()
が呼び出される度に Bar()
関数が上書きされる』ということである。
なので、Foo()
を呼び出す度に F()
の結果がリセットされてしまう。
function! Foo() let x = 0 function! Bar() closure let x += 1 return x endfunction return function('Bar') endfunction let F = Foo() echo F() " => 1 echo F() " => 2 echo F() " => 3 " Bar() 関数が書き換えられる let F2 = Foo() " F と F2 は同じ関数を参照してるため、意図した結果が帰ってこない echo F() " => 1 echo F2() " => 2 echo F() " => 3
上記の場合は、2回目に Foo()
を呼び出したら F()
の結果が初期化されてしまう。
また、F
と F2
には同じ関数を参照してしまうので、意図した結果が帰ってこない。
もし、こういう風に関数を再定義して closure
を利用したいのであればローカル関数を定義したり、関数をコピーする必要がある。
そもそも Vim script で『関数内で関数を定義する』というのは辞書関数を除いて基本的にやるべきではない。
[回避方法]
要は『関数内でグローバル関数(or スクリプトローカル関数)』を定義しなければよい。
こういう時こそ lambda
が利用できる。
function! Foo() let x = 0 return { -> [execute("let x += 1"), x][-1] } endfunction let F = Foo() echo F() " => 1 echo F() " => 2 echo F() " => 3 " 毎回別の関数が生成されるので F と F2 は別の関数を参照する let F2 = Foo() echo F() " => 4 echo F2() " => 1 echo F() " => 5
また、辞書関数を利用しても回避することができる。
function! Foo() let x = 0 let func = {} function! func.call() closure let x += 1 return x endfunction return func.call endfunction let F = Foo() echo F() " => 1 echo F() " => 2 echo F() " => 3 " 毎回別の関数が生成されるので F と F2 は別の関数を参照する let F2 = Foo() echo F() " => 4 echo F2() " => 1 echo F() " => 5
[まとめ]
closure
自体はとても便利な機能であるが、closure
を利用する場合は、『その関数を定義する場所や定義した関数を誰が参照するのか』を注意して使う必要がある。