Vim script の型について

Vim Advent Calendar 2011 43日目の記事となります。
もはや Advent Calendar がなんなのかわからなくなってきましたね。
明日以降、誰が書くか未定なので、もし書いた or 書きたい人がいれば ATND のコメントに投稿して下さい。
特にもう日付を気にする必要はないと思うので、気が向いたときにでも気軽に参加してもらえればと思います。

[まえがき]

さて、3回目となる今回は Vim script について書きたいと思います。
Vim script は動的型付け言語です。
なので普段はあまり型を意識することがないと思いますが、ちょっと型について書いてみようと思います。

[Vim script で型]

Vim script では型を書くことはありませんが、type({expr}) で変数から型番号を取得することが出来ます。
現在、Vim script で定義されている型は6種類あり、以下のような型になります。

型名 型番号
type(0) 数値 0
type("") 文字列 1
type(function("tr")) Funcref 2
type([]) リスト 3
type({}) 辞書 4
type(0.0) 浮動小数点数 5

[値の代入]

すでに定義されている変数へ値を代入する際に注意する必要があります。
例えば、数値から辞書を再代入しようとするとエラーになります。

let var = 0
let var = {}    " E706: 変数の型が一致しません: var


このように同じ変数名を使いまわしたい場合は、unlet を使用して、一度変数を削除する必要があります。

let var = 0
unlet var        " 変数を削除
let var = {}    " ok


さて、普段はあまり上記のような使い方はしない思いますが、次のように異なる型のリストを for 文で走査する場合に問題になります。

let list = ["homu", 42, 3.14, [1, 2, 3]]
for var in list
    echo var
    " E706: 変数の型が一致しません: var
endfor


これも先ほどと同じように var に異なる型の値を代入しようとしているためにエラーになります。
ですので、ループの最後に unlet を使用して var を削除する必要があります。

let list = ["homu", 42, 3.14, [1, 2, 3]]
for var in list
    echo var
    " 最後に unlet する
    unlet var
endfor


これで問題なく動作するようになりました。

[has_key]

さて、次のような関数を考えてみましょう。

  • 1.辞書に"line"というキーがあればその値を返す
  • 2.それ以外の場合は引数をそのまま返す


そのまま書くとこんな感じになると思います。

function! s:get_line(value)
    return has_key(a:value, "line") ? a:value.line : a:value
endfunction

let dict = {}
let dict.line = 42
echo s:get_line(dict)
" 42


しかし、has_key には辞書しか渡すことが出来ないので、辞書以外の型を渡すとエラーになります。

echo s:get_line("nothing")
" 辞書型が必要です


なので、次のようにまず辞書かどうかのチェックを行う必要があります。

function! s:get_line(value)
    return type(a:value) == type({}) && has_key(a:value, "line")
\          ? a:value.line
\          : a:value
endfunction

let dict = {}
let dict.line = 42
echo s:get_line(dict)
" 42
echo s:get_line("nothing")
" nothing


これで問題なく動作するようになりました。

[関数の多重定義]

Vim script で関数の多重定義を行うことが出来ません。
しかし、次のような加算処理を考える場合に多重定義を行いたい事があります。

" 引数の加算値を返す
function! s:plus(a, b)
    retur a:a + a:b
endfunction

" ok
echo s:plus(10, 20)
" 文字列の場合は???
echo s:plus("homu", "mado")


Vim script では文字列の結合は『.』を使用するのでこのままではうまく動作しません。
本来であれば、関数内で type を使って引数の型をチェックを行います。
が、今回は {} を使用して関数名の末尾に型番号を付けて多重定義っぽく書いてみたいと思います。

" 型を定義
let s:int    = type(0)
let s:string = type("")


" 関数の末尾に型を付ける
function! s:plus{s:int.s:int}(a, b)
    return a:a + a:b
endfunction

function! s:plus{s:string.s:string}(a, b)
    return a:a . a:b
endfunction

" 異なる方の場合は、第一引数の型に合わせてみたり
function! s:plus{s:int.s:string}(a, b)
    return a:a + a:b
endfunction

function! s:plus{s:string.s:int}(a, b)
    return a:a . a:b
endfunction


function! s:plus(a, b)
    " 関数の呼び出しに type を使う
    return s:plus{type(a:a).type(a:b)}(a:a, a:b)
endfunction

echo s:plus(1, 2)
" 3
echo s:plus("homu", "mado")
" homumado
echo s:plus("homu", "123")
" homu123
echo s:plus(25, "11")
" 36


こんな感じで使うことが出来ます。
条件分岐を書くよりは、すっきりとしているコードになるのではないでしょうか。
今回はミニマムな実装だったので引数の数やユーザ側で定義した型などは考えませんでしたが、こういう使い方も面白いと思います。
こんな使い方も出来る Vim script は変態楽しいですね。
今年はもっと Vim scripter が増えて欲しいですね!