読者です 読者をやめる 読者になる 読者になる

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() の結果が初期化されてしまう。 また、FF2 には同じ関数を参照してしまうので、意図した結果が帰ってこない。 もし、こういう風に関数を再定義して 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 を利用する場合は、『その関数を定義する場所や定義した関数を誰が参照するのか』を注意して使う必要がある。