Help us understand the problem. What is going on with this article?

Vim 8.0 Advent Calendar 1 日目 関数機能の強化

More than 3 years have passed since last update.

この記事は 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

関数のスコープに注意してください。関数内であろうとも、関数自体のスコープは関数ローカルにはなりません。グローバル関数を定義すれば、それはグローバル関数になります。恐らくこれは歴史的な理由によるものだと思われます。

もしくはラムダを使うことでもクロージャを作成できます。ラムダを使う場合、ラムダ内の式が静的にパースされて、外側の変数を参照していた場合にクロージャと判定されます。よって、例えば :executeeval() などで動的に参照されるだけの場合、クロージャにはならず外側の変数は参照できません。

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() を使う必要があります。

thinca
「ご趣味は」 「Vimを少々…」
http://thinca.hatenablog.com/
aktsk
株式会社アカツキは、スマートフォンゲームの企画開発を中心に事業を展開しております。創業以来全てのゲームを内製しているため、高い技術ノウハウが蓄積されています。今後は、新規事業の立ち上げも行ってまいります。
http://aktsk.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away