Vim script でもラムダを使いたい!

この記事は Vim Advent Calendar 2012 227日目の記事になります。


ラムダですよ!ラムダ!
例えば、Vim script でもオレオレ fold を作りたい時やコールバック関数を受け取りたい時にラムダを使いたい!!と思う事が多いです。
そんな時に reti.vim を使用すればラムダ(のようなもの)を使うことが出来ます。


また Vim script でラムダに関しては去年の Vim Advent Calendar で mattn さんが少し触れています。


reti.vim とはコンセプトが違いますが、こちらも読んでみると面白いです。

[インストール]

reti.vim を使用します。
また、reti.vim を使用するにあたって chained.vim もインストールしておく必要があります。

NeoBundle "osyo-manga/vim-reti"
NeoBundle "osyo-manga/vim-chained"

[簡単な使い方]

ラムダといっても Vim script では構文としてラムダを定義する事は出来ないので、文字列から関数を生成する事になります。

" 第一引数と第二引数を加算して返す関数を定義
" 関数の引数には a: を使用
let F = reti#lambda("a:1 + a:2")
echo F(2, 5)
" => 7

echo F(7, -6)
" => 1

echo reti#lambda("a:1 - a:2")(4, 9)
" => -5


このように文字列で式を記述し、その式を評価する関数の参照を返します。
イメージとしては Vim script の組み込み関数である map() や filter() に渡す文字列ような感じです。


また、上記のような式以外にもコマンドを評価することが出来ます。

" コマンドを評価する場合は ":" から始まる必要がある
let Echo = reti#lambda(":echo a:1")

call Echo("homu")
" => "homu"

call Echo(42)
" => 42


これを利用すれば map() や filter() 内でコマンドを評価する事も出来ます。

call map(range(5), "reti#lambda(':echo a:1')(v:val)")
" =>
" 0
" 1
" 2
" 3
" 4

[式の中でローカル変数やスクリプトローカル変数を参照する]

さて、組み込み関数である map() や filter() では式の中で直接ローカル変数やスクリプトローカル変数を記述して参照する事が出来ます。

let s:mami = "mami"
echo map(range(5), "s:mami")
" => ['mami', 'mami', 'mami', 'mami', 'mami']

function! s:func()
    let value = 1
    echo filter(range(5), "value < v:val")
    " => [2, 3, 4]
endfunction
call s:func()


しかし、reti.vim ではこのように外部の変数を直接参照する事はできません。
外部の変数を参照したい場合は reti#lambda() の引数に明示的に使用する変数を渡す必要があります。

let s:mami = "mami"

" 第二引数に変数の辞書を渡す
" その辞書のキーを変数を式の中で使用する事が出来る
let F = reti#lambda("mami . a:1", { "mami" : s:mami })
echo F("mado")
" => mamimado

function! s:func()
    let value = 3
    let F = reti#lambda("value + a:1", { "value" : value })
    echo F(9)
    " => 12

endfunction
call s:func()


また、Vim script での s: がスクリプトローカル変数の辞書や l: がローカル変数の辞書である事を利用すれば次のように簡潔に記述することも出来ます。

let s:mami = "mami"
let s:homu = "homu"

" スクリプトローカル変数全てが使用出来る
let F = reti#lambda("mami . a:1 . homu", s:)
echo F(" + ")
" => ami + homu

function! s:func()
    let value1 = 3
    let value2 = 5

    " ローカル変数も同様
    let F = reti#lambda("(value1 + a:1) * value2", l:)
    echo F(9)
    " => 60

    " 両方参照する事も可能
    let F = reti#lambda("(value1 + a:1) . mami", l:, s:)
    echo F(9)
    " => 12mami
endfunction
call s:func()


外部の変数を参照する場合はこのようにして使用します。
また、let を使用して変数を式の中で書き換える事も出来ます。

let s:value = 0
let F = reti#lambda(":let value = 42", s:)

echo s:value
" => 0

call F()
echo s:value
" => 42

[自身を参照する]

再帰処理等を行いたい時に自身を参照したい場合があると思います。
式の中で Self を使用する事で自身を呼び出す事が出来ます。

" Self で自身を評価
echo reti#lambda("a:1 == 1 ? a:1 : a:1 * Self(a:1 - 1)")(4)
" => 42

" クイックソートをワンライナーで記述したり
let list = [3, 2, 7, 1, 9, 8, 4, 6, 5]
echo reti#lambda('a:1 == [] ? [] : Self(filter(copy(a:1), "v:val < a:1[0]")) + [a:1[0]] + Self(filter(copy(a:1), "a:1[0] < v:val"))')(list)
" => [1, 2, 3, 4, 5, 6, 7, 8, 9]

[二項演算子を関数]

さて、reti.vim には式を関数化する以外にもいくつか機能があります。
例えば二項演算子を渡した場合、2つの引数を受け取る関数の参照を返します。

" reti#lambda("a:1 + a:2") と等価
let F = reti#lambda("+")
echo F(3, 2)
" => 5

" 比較演算子とか
let F = reti#lambda("==#")
echo F("homu", "mami")
" => 0
echo F("mami", "mami")
" => 1


これを利用すると組み込み関数である sort() を次のように簡潔に既述する事も出来ます。

echo sort([3, 6, 1, 9, 2, 7, 5, 4, 8], reti#lambda("-"))
" => [1, 2, 3, 4, 5, 6, 7, 8, 9]

[スクリプトローカル関数を SID 付きの名前へ変換]

条件付きですが、スクリプトローカル関数を SID 付きの関数へと変換を行うことが出来ます。
条件はスクリプトローカル関数内で使用する事。

function! s:minus(a, b)
    return a:a - a:b
endfunction

" NG
" echo reti#function("s:minus")(1, 2)

function! Func()
    " NG
    " echo reti#function("s:minus")(1, 2)
endfunction

" スクリプトローカル関数で呼び出した場合、SID 付きの関数参照として返す
function! s:func()
    " OK
    let F = reti#lambda("s:minus")
    echo F
    " => <SNR>xxx_minus

    echo F(1, 2)
    " => -1

    " グローバル関数の場合は function() と等価
    let F = reti#lambda("Func")
    echo F()
    " => 0
endfunction
call s:func()
<


これはスクリプトローカル関数をスクリプトの外で評価する場合に有効です。
例えば、スクリプトローカル関数をコールバック関数として渡したい等。
(ただし、最新版の Vim では function() にスクリプトローカル関数が渡せるようになったみたいですが(未確認

[辞書関数を関数化]

辞書と辞書関数名を渡した場合、その辞書関数を評価する関数の参照を返します。

let s:dict = {
\   "value" : 0
\}

function! s:dict.count()
    let self.value += 1
endfunction

let s:Count = reti#lambda(s:dict, "count")
call s:Count()
call s:Count()
call s:Count()
call s:Count()
echo s:dict.value
" => 4


と、いう感じで reti.vim を使用すれば Vim script でなんちゃってラムダを使うことが出来ます。
実際のところ、このプラグイン単体ではエディタとして何もしないんですが、トイプラグインとしては面白いんじゃないかと思います。
他にも vim-reti/example にいくつか example が置いてあるので気になる人は見てみるといいと思います。