時代遅れひとりFizzBuzz祭り Vim script編

時代遅れひとりFizzBuzz祭り、今回はVim scriptだ。前回の秀丸エディタ同様、テキストエディタの組み込みスクリプト言語だ。ついでに言うと私が使用しているテキストエディタにて「プログラミング言語的な組み込み言語」「Lisp系じゃない」の2点を満たしているもの、という共通項もある。

TeraPadはマクロ言語を持たないし、K2Editorのマクロはキーボードマクロでプログラミング言語っぽさは少ない。otbeditはSchemeで、xyzzyNTEmacsLispで拡張する。

さて、エディタの組み込み言語とはいえ秀丸マクロとVim scriptとでは随分と立ち位置が異なるように思う。

秀丸マクロの起源は分からないのだが、秀丸エディタが主であり、秀丸マクロはエディタをサポートする為に取り付けられた(又は初期設計段階からその目的で存在した)機能だと考えてよいだろう。

一方Vim scriptは、vi由来のexコマンドを拡張したものだ。viはラインエディタであるexを改良してスクリーンエディタにしたもので、exコマンドはラインエディタの時代から引き継がれてきたUIであり遺産だと言える。そんなexコマンドを元とするVim scriptは、Vimを拡張する言語であると同時にVimをラインエディタ的に操作するコマンド体系でもあるといえる。つまりコマンドの羅列なのだ。

21世紀になってもラインエディタの時代の呪縛から逃れられない我々は、重力に魂をとらわれたオールドタイプなのだろうか――21世紀になってからプログラミングを始めた私は香り屋版Vim 7.3.154を使いながら今日も自問するのであった、まる。

まずは秀丸マクロの時と同様に、一般的だろうスタイルのFizzBuzzを目指してみた。

let s:i = 1
while s:i <= 100
  if s:i%15 == 0    | echo "FizzBuzz"
  elseif s:i%3 == 0 | echo "Fizz"
  elseif s:i%5 == 0 | echo "Buzz"
  else              | echo s:i
  endif

  let s:i += 1
endwhile

値をカウントアップしていくスタイルのforループは無いので、whileでループしている。ifの羅列中に `|' があるが、これは複数のコマンドを1行に記述する場合に使用するらしい*1

変数に `s:' というプレフィックスを付けているが、これはその変数のスコープを表す。`s:' を付けるとスクリプトローカル変数となり、:sourceコマンドで読み込んだスクリプトファイル内でのみ有効なローカル変数となる。

内部変数のスコープは合計8種類ある。こう書くと何やら複雑なように感じるが、実際はバッファ・ウィンドウ・タブといったVim内部でのコンテキストの違いがスコープとなっている為、スコープの種類が増えているだけだ。そういった部分に立ち入らない限り、ちょっとしたスクリプトを記述する分には2〜3のスコープを気にするだけで十分なようだ。

forループはリストに対する繰り返しを行う。指定回数だけループしたい場合は、数列リストを生成する関数rangeを使ってforループした方がよい。この方法はヘルプファイルでも紹介されているので、恐らく由緒正しい(?)方法なのだろう。

for i in range(1, 100)
  let s:val = i%3 == 0 ? "Fizz" : ""
  if i%5 == 0 | let s:val .= "Buzz" | endif
  echo s:val == "" ? i : s:val
endfor

三項演算子や複合演算子もある。文字列を結合する場合は `.' を使う。

配列――というかリストはこう定義する。

let s:msg_table = [0, "Fizz", "Buzz", "FizzBuzz"]
for i in range(1, 100)
  let s:msg_table[0] = i
  echo s:msg_table[(i%3 == 0) + (i%5 == 0) * 2]
endfor

要素を参照する `[]'、リストを連結する `+' 以外の操作には組み込み関数を使うようだ。リスト操作用の関数が多数定義されている。

リストのほかに辞書(連想配列)もある。`{}' で囲むところやキーと値を `:' で区切って記述するところはJavaScriptのオブジェクトのリテラル表記に似ている。

for i in range(1, 100)
  let s:msg_table = {"0": i, "1": "Fizz", "2": "Buzz", "3": "FizzBuzz"}
  echo s:msg_table[printf("%d", (i%3 == 0) + (i%5 == 0) * 2)]
endfor

辞書にも豊富な組み込み関数が定義されている。

辞書のキーのデータ型は文字列だけだ。

for i in range(1, 100)
  let s:msg_table = {0: i, 1: "Fizz", 2: "Buzz", 3: "FizzBuzz"}
  echo s:msg_table[(i%3 == 0) + (i%5 == 0) * 2]
endfor

このコードは一見して数値をキーとして使用しているように見えるが、実際には数値から文字列に自動的に変換されている。

関数も定義できる。関数名は組み込み関数と区別する為に大文字で始まる必要があるらしい。

function FizzBuzz(n)
  let msg_table = [[a:n, "Buzz"], ["Fizz", "FizzBuzz"]]
  return msg_table[a:n%3 == 0][a:n%5 == 0]
endfunction

for i in range(1, 100)
  echo FizzBuzz(i)
endfor

関数内で引数にアクセスする場合は `a:' というプレフィックスを付ける。関数内でプレフィックス無しで変数を定義すると関数のローカル変数となる。変数名の衝突を避けるために明示的に `l:' を付けてもよい。

スクリプトを再読み込みする可能性があるなら、関数を再定義可能なようにしたほうがよいだろう。`function!' と `!' を付けると、既存の関数を上書き定義できる。付けていない場合、例えばsourceコマンドで繰り返しスクリプトを実行していると、2回目以降の実行時に関数が定義済みだという内容でエラーが発生する。

function! s:fizzbuzz(n)
  let msg_table = [[a:n, "Buzz"], ["Fizz", "FizzBuzz"]]
  return msg_table[a:n%3 == 0][a:n%5 == 0]
endfunction

echo join(map(range(1, 100), "s:fizzbuzz(v:val)"), "\n")

ちなみに関数を定義する時に `s:' を付けるとスクリプトローカルな関数になる。この場合は関数名を大文字で始める必要はないようだ。

それにしてもVim scriptはコマンドの羅列とは思えない程度にはプログラミング言語らしさを醸し出していると思う。コマンドの羅列というとシェルスクリプトやTclが思いつくのだが、それらの独特さに比べるとVim scriptは普通の言語に近い。

とはいえVim scriptの本筋が「Vimを操作するコマンドを並べ立てる」という所にあることを忘れてはならない。Vim内部のコンテキストにはどのようなものがあり、そのそれぞれを操作したり情報を引き出したりするにはどのような方法があるか把握した上で、ようやくVim scriptの出番となる。単にVim scriptの文法を学ぶだけでは大したことはできない。

まあ、この辺りは特定のツールを拡張するマクロ言語の類には付き物の話だろう。

*1:但し常にこの方法が使えるわけではないらしい……。