Subscribed unsubscribe Subscribe Subscribe

sci

最果て風呂

Vim の変数のスコープについて調べたら「わかってない」ということがわかった

前口上

Emacs Lisp については良く知らないのだが、定型句を置換するスクリプトを作った。これは CSV のような外部ファイルから単語のペアを読み込んで連想リストを作成し、各コンスセルの car で検索し cdr で置き換えるというものである。

連想リストはスクリプト内でも外側(let 内ではない)で setq しており、Emacs が起動している間はメモリに残っていて、また、どこからでもアクセスすることができる。

連想リストの元となる辞書(外部ファイル)の登録数が多くなってくると、単語を多重に登録してしまうことが増えた。そこで CSV の状態で sort, cut, uniq, sed を用いたシェルスクリプトを作成し、シェル上で確認をするようにした。

とある会話の流れから、連想リストを持っているのだから、それを利用して Emacs 内で「単語数・重複数・重複している内容」を調べて表示してしまえば良いのでは?ということになった。20 行程度だが、上記の命題を実現する関数を作成した。これは置換スクリプトとは別のファイルに作成し、Emacs 内に eval で読み込ませているのだが、期待通りの動作をしている。

さて、Vim script でも同じような置換スクリプトを作成しており、こちらでも Vim 内で単語数を表示できれば便利だと思い、ヘルプを見ながら作成することにした。作業にとりかかる前は「二次元配列はグローバルで持ってるから Emacs と同じようにやれば簡単にできそうかなぁ」などと思っていたのだが、実際にスクリプトを見てみると「頭文字 s:」が使われていた。

過去の自分を忘れている自分。当初はすべて g: で作成し、試行錯誤で b: や s: にしていったように思う。色付けに用いるキーワードリストは developerworksVim 記事を参考にして、当初は w: にしていたことは覚えているのだが、作業記録を残していないので、どういった経緯でそうしたのか思い出すことができない。

変数の寿命や範囲について無知な自分が曝け出されてしまった瞬間である。参照渡しや値渡しなどについてもわかっていない。変数ではなく関数についてではあるが、過去に twitter で下記のようなつぶやきをした。

Vim script の関数は大文字から始める決まりになっているけど、他の人のソースを見るとスクリプトローカル関数を使っている人ばかりだ。でもこれって意味がわからん。:source したらいつでもどこでも呼び出せるじゃない?これはシステム大域関数になってるってことじゃないかのな?

これに対して、magicant さんに

s: で始まる関数も実は大域関数ですが、 12_hoge みたいに変なプリフィクスが付くので名前は被りません

というヒントを頂いたのだが、まったく頓珍漢な返事をしている自分なのであった。また、Vim-jp では koron さんに「ローカルって言葉を軽々しく使うもんじゃない。正しく理解し、正しく用いるべし。(意訳)」と諭されてしまうのであった。

前口上が長くなってしまったが、先にも述べた「単語数表示スクリプト」を作成しながら Vim に多数ある変数のスコープについて理解していくことが、この文章の本題である。

まずは同じファイル内で

Vim でも Emacs と同じように置換スクリプト本体とは別のファイルに作成しようと思っているのだが、まずは本体と同じファイルに関数を書いていくことにする。ここで成果が見られれば別ファイルにしていこう。

本体では二次元配列を `s:car_cdr_arr` という名前で持つようにしているのだが、`CreateDict(use_jisyo)` という関数を一度は実行しておかなければならない。これをすることによって Vim 内のどこかに `s:car_cdr_arr` という変数が保持されることになる。たぶんこれは Vim を終了するまではメモリに残っているのだと思う。

二次元配列は関数を実行する度に毎回作り直しているのだけど、`let s:car_cdr_arr = []` を実行することによって古い `s:car_cdr_arr` はメモリから消えてなくなり、新たにメモリに確保されると自分で勝手に思っている。もしかして束縛が解除されただけで、古い方はメモリにゴーストとして残ってるのかな?

ちなみにハッシュにしていないのは、置換する順番が重要だからなのです。Vim の辞書型は Ruby とは違い、Python などと同じように「順序を保持しない」もしくは「保証されない」だった(と思ったけど、どうだったかなぁ...)。

さて、配列の大きさを調べるには「Vim スクリプト基礎文法最速マスターhttp://d.hatena.ne.jp/thinca/20100201」によると `len()` を使うみたいだ(`:help` を見ろというお叱りがありそうだが...)。

function! CheckArrayDup()
call CreateDict(s:kana_jisyo)
echo "単語数: " . len(s:car_cdr_arr) . " なのだ"
endfunction

を書き込んで確認。めでたくコマンドラインに表示された。

:source %
:call CheckArrayDup()
" => 単語数: 1865 なのだ

次は配列の重複を調べること。どうやら `uniq` のようなメソッドはないみたい。`uniq` して元の配列との差分を取ってそれを表示する...という方法は使えない。これは Emacs Lisp でやったのと同じ方法で実装することになりそうかな?空の配列を 2 つ用意して、`s:car_cdr_arr` から `independent_list` にどんどん追加していく。もし、追加しようとして同じ要素がすでにある場合は `duplicated_list` に追加する。

マップ関数は `for ... in` を使うとして、`exist?` or `member` についてはどうやって調べるのだろう?`:help exis TAB` で調べると `exists()` が出てきた。これで行けそうかな?どうやら違うみたい。`count()` ならいけるか?リストの要素を連結するには `join(list, '')` が使えそう。

ファイルを分けてみる

やめやめ。当初の目的は変数のスコープについて調べるというものであったので、先にそちらを調べなくては。ということで上の関数を「そのままの状態で」別のファイルに分けて読み込ませてみる。もちろん Vim は再起動。

:source %
:call CheckArrayDup()

Error detected while processing function CheckArrayDup:
line 1:
E121: Undefined variable: s:kana_jisyo
E116: Invalid arguments for function CreateDict
line 2:
E121: Undefined variable: s:car_cdr_arr
E116: Invalid arguments for function len(s:car_cdr_arr) . " なのだ"
E15: Invalid expression: "単語数: " . len(s:car_cdr_arr) . " なのだ"

予想していたとおりエラーになってしまった。「んなもの知るかっ」ってさ。

E121: Undefined variable: s:kana_jisyo
E121: Undefined variable: s:car_cdr_arr

これはどういうことを意味するのだろうか? s: 変数というのは「その同じスクリプトファイルに書かれている関数からしかアクセスができない」ということなのだろうか?だって先程は「同じファイルの中にある関数からアクセスすることができた」もんね。

ぜんぶ `g:` 変数にしちゃう

実験ということで、エラーの元になっている変数を `g:kana_jisyo`, `g:car_cdr_arr` のように `g:` 変数にしてみる。どちらのスクリプトも変更しておいてから再起動。それぞれ `source %` をして `:call CheckArrayDup()` を実行。これは問題なく動作した。

`w:` 変数にしてみる

変数を両方追うのは繁雜なので、`g:kana_jisyo` はそのままにして `g:car_cdr_arr` のみを `w:` 変数にして実験してみる。同じウインドウでファイルを開いて、それぞれを `:source %` にて読み込み。これも問題なく動作した。

次は本体スクリプトを開いて `:source %` し、`C-w s` で分割した別のウインドウからテストスクリプトを開いて `:source %` し実行する。予想ではエラーになると思っていたけど、問題なく動作してしまった。自分の `w:` 変数に対する認識が間違っていたことになる。どういうことだろう?ウインドウって別の意味のウインドウのことかな?

`b:` 変数にしてみる

悩んでも答えが出ないので先に進んでしまう。`b:` 変数にしてから、先程と同じようにそれぞれ読み込んで `:source %` を実行。今回はバッファが違うのだからエラーになるはず。おや?成功してしまった。どういうことなんだろう?

あ!テストスクリプトの中で

call CreateDict(g:kana_jisyo)

を呼んでいるから、この時点で `b:car_cdr_arr` が設定されるので、このバッファ内でアクセスできるようになってるのかな?試しにテストスクリプトを `:source %` したあとにさらに別のバッファに移動して実行してみる。成功してしまった...。ますますわからなくなってしまった。

`:source` したそのバッファが存在していると `b:` 変数も存在してアクセスできるのだろうか?このバッファを削除してみたらどうだろう? うわ〜、これも成功してしまう。

わからん

実験ではことごとく自分の予想と反した結果が得られてしまった。これは現状での私の理解が誤っているということだ。テスト方法に誤りがあったのかもしれない。どちらにしても上記に書いたことが usr_41 および eval をちゃんと読む前の段階での私の認識だ。

issue の中で koron さんが指摘されていた「ローカルという言葉の罠」に見事にはまっているみたい。まさしくローカルという言葉に私自身が束縛されてしまっているのであった(^ω^;)。ぜひとも達人 Vimmer によって書かれた、ミニサンプルによる補足が欲しいところである。それを help に含めるか否かの話は別として。

おまけ

変数のスコープを調査することは挫折してしまったので、先に単語数を調べるスクリプトを作った。これは辞書名が決め打ちなので汎用性がない(辞書は他にもあるので)。関数に引数を渡して `a:` 変数の登場してもらうことになるの。いずれにせよ、ちゃんと把握していなければ使えないということです。

" 二次元配列の重複を調べる
function! CheckArrayDup()
call CreateDict(s:kana_jisyo)
let s:independent_list =
let s:duplicated_list =

let s:duplicated_word = []

for cons in s:car_cdr_arr
if 0 == count(s:independent_list, cons)
call add(s:independent_list, cons)
else
call add(s:duplicated_list, cons)
endif
endfor

for cons in s:duplicated_list
call add(s:duplicated_word, cons[0])
endfor

echo "登録数: " . len(s:car_cdr_arr) .
\ ", 有効: " . len(s:independent_list) .
\ ", 重複: " . len(s:duplicated_list) .
\ ", 重複単語: " . join(s:duplicated_word, ', ')
endfunction

実行してみると下記のような結果が表示された。

:call CheckArrayDup()
登録数: 1865, 有効: 1865, 重複: 0, 重複単語: