読者です 読者をやめる 読者になる 読者になる

Vim script で AtCoder に参戦する方法

先週、進捗キャンプという知り合いで集まって進捗を出す会に行ってきて、そこで @ さんと表題の件について色々話した。
その後個人的に手法をカイゼンしたりしたので、結果をまとめておく。

AtCoder とは

http://atcoder.jp/
私自身も「とは」と言って語れるほど詳しくはないので簡単に説明すると、主に日本人向けの競技プログラミングサイト。
問題は全て日本語で、定期的に様々なコンテストが開催されている。問題の難易度が低い初心者向けのコンテスト(AtCoder Beginner Contest 通称 ABC)なんかもあるので、競技プログラミングの入口としては最適だ。

AtCoder の対応言語

競技プログラミングでは、ソースコードを提出し、提出されたコードが正しく動くかどうかサーバ側で実際に動かして検証を行う。つまり、サーバ側に各言語の処理系が必要であり、競技プログラミングで使用できる言語はその点で制約がかかる。
どの言語が使えるかは競技プログラミングによって様々であるが、AtCoder はかなり多くの言語に対応している。
しかし、Vim script は対応言語には含まれていない。
それもそのはず。Vim script は言語としてマイナーという点を除いても、とある問題がある。

競技プログラミングの問題形式と Vim script

競技プログラミングにおける問題は、多くの場合、標準入力と標準出力を使う。
標準入力から問題のデータを受け取り、問題を解いて、結果を標準出力に出力させる。
AtCoder も例に漏れずこの形式である。
標準入出力は多くの言語が標準で扱えるので、この形式にすることで多くの言語に対応することができるためだと思われる。
しかし、Vim script はテキストエディタ Vim を拡張するための言語。標準入出力を直接扱うことは、できない。

ではどうするか

AtCoder の対応言語の1つに、Bash がある。
Bash は、それ自身にもそれなりの演算能力はあるが、どちらかと言うと外部プログラムを呼び出す能力に長けている。
実際、awk や bc なんかがよく使われたりするらしい(よく知らないけど)。
つまり、この Bash の環境に Vim が入っていれば、あとは標準入出力を Bash 側で面倒を見ることによって、Vim script が使えることになる。
試したところ Vim はちゃんと入っているようだった*1。これで Vim script で問題が解ける。

Vim script で問題を解く

AtCoder には練習用のコンテストがあるので、そこにある練習問題を解いてみる。
http://practice.contest.atcoder.jp/tasks/practice_1

vim -u NONE -i NONE -N -n -e -s -S <(cat <<EOF
function! s:main(input) abort
  let a = a:input[0]
  let [b, c] = split(a:input[1], ' ')
  let s = a:input[2]
  return (a + b + c) . ' ' . s
endfunction

let s:input = getline(1, '$')
enew
put =s:main(s:input)
1 delete _
%print
EOF
) <(cat)

結果はこちら
ちゃんと解けてる。

何をしているのか

まず、このコード自体は Bash であるが、事実上その大部分は Vim script である。
ちょっとずつ分解していくと、まず vim を起動するコードは Vim script の部分を省略すると以下のようになる(... の部分が省略部分)。

vim -u NONE -i NONE -N -n -e -s -S <(...) <(cat)
  • -u NONE
    • vimrc ファイルを読み込まない。
  • -i NONE
    • vininfo ファイルを作成しない。
  • -N
    • vi 互換モードをOFFにして(つまりVimとして)起動する。
  • -n
  • -e -s
    • バッチモードで起動する。詳細は割愛。(気になる人は :help -s-ex を参照)
  • -S {ファイル}
    • 起動後にファイルを Vim script として実行する。
  • <(cat)
    • Vim で開くファイルの指定。<(...) はプロセス置換という bash の機能で、コマンドの結果をファイルとして渡すことができる。

さて、Vim を起動していることはわかったので、次は Vim script 部分。

function! s:main(input) abort
  let a = a:input[0]
  let [b, c] = split(a:input[1], ' ')
  let s = a:input[2]
  return (a + b + c) . ' ' . s
endfunction

let s:input = getline(1, '$')
enew
put =s:main(s:input)
1 delete _
%print

ちょっとずつ見ていく。

function! s:main(input) abort
  let a = a:input[0]
  let [b, c] = split(a:input[1], ' ')
  let s = a:input[2]
  return (a + b + c) . ' ' . s
endfunction

わかりやすくするため、問題を解く部分を関数に分けた。
引数の input は、入力のテキストが行単位で配列で渡ってくる。
結果を文字列か、行単位の配列で返す。

let s:input = getline(1, '$')

入力はファイルとして渡ってきており、バッファに開かれている。
バッファの全行を配列として s:input に保存している。

enew

output 用の新しいバッファを開く。

put =s:main(s:input)
1 delete _

結果をバッファに展開している。
空のバッファに put でテキストを置くと1行目に空行が残ってしまうので、delete でこの余計な空行を消している。

%print

バッチモードの Vim では、print などのいくつかのコマンドの結果は標準出力へ出される。
%print でバッファ全体を標準出力へ結果として出力している。
ちなみにこの方法だと、最後に改行のない出力が行えない*2が、大抵の競技プログラミングの問題は最後に改行を出力させるので、問題になることはないだろう。

解く環境を整える

テンプレート

こうして見るとわかる通り、実際に触るのは s:main() 関数の部分だけであり、他の部分はテンプレートだ。
テンプレート系のプラグインを使って、テンプレート化しておくと便利だろう。
ちなみに私は template.vim を使っている(宣伝)。

Vim script 部分だけ編集する

ファイル全体としては Bash だけど、編集したいのは Vim script だ。main の部分だけを、filetype=vim で編集したい。
そんな時に便利なのが partedit.vim だ。
バッファの一部を別のバッファで開き、別のファイルタイプで編集できる。保存すると元のバッファに適用される。

https://i.gyazo.com/14a70ff2a7148b9e1507170bda083710.gif

上の動作例では :Partedit コマンドを引数なしで動かしているが、そうするためにはオプションの設定が必要だ。

" 部分を編集するバッファを開くコマンドの指定。これは縦分割したい場合の例 (デフォルトだと現在のウィンドウにそのまま開かれる)
let g:partedit#opener = 'vsplit'
" 部分を編集するバッファの filetype。デフォルトだと元のバッファと同じ filetype が適用される。
let g:partedit#filetype = 'vim'

ファイルタイプは常に同じだと別のシーンで使いたい場合に困ると思うので、localrc.vim などを利用して、b:partedit_filetype を設定するといいだろう。

テストケースを実行したい その1

答えが正しいか、提出する前に手元で実行して確認したいだろう。
開いているファイルを実行したい、と来れば、quickrun.vim が便利だ。
問題の入力を標準入力で与える必要があるが、幸い quickrun.vim は標準入力にも対応している。input オプションを設定すればよい。

" input.txt ファイルの中身を標準入力として使う
QuickRun -input input.txt

" = で始めることでファイルではなく入力文字列を直接与えられる
QuickRun -input =input-text
" i レジスタの中身を標準入力として使う
QuickRun -input =@i
" b:input 変数の中身を標準入力として使う
QuickRun -input =%{b:input}

" 事前に設定しておけば毎回指定する必要はない
let g:quickrun_config = {'_': {}}
let g:quickrun_config._.input = '=@i'
let g:quickrun_config._.input = '=%{b:input}'

" 実行する際に変数に値を入れれば OK
let b:input = "1\n2 3\ntest\n"

print デバッグするには、put コマンドを使えばよい。
最終的なバッファの内容が出力結果になるので、put コマンドでバッファに行を足してやる。

let hoge = 'debug'
put =hoge
" put の中では " はコメント扱いになり使えないので注意
put ="foo"  これはコメントになるので動かない
put =\"foo\"  " これなら動く
テストケースを実行したい その2

実は、もっと楽な方法がある。ここで s:main() を切り分けておいたのが役に立つ。
単純に s:main() を呼んでしまえば良い。

function! s:main(input) abort
  let a = a:input[0]
  let [b, c] = split(a:input[1], ' ')
  let s = a:input[2]
  return (a + b + c) . ' ' . s
endfunction

echo s:main(['1', '2 3', 'test'])

これで OK だ。ただし、この状態で Bash としてこれを実行しても結果を見ることはできない。
しかし先ほどの partedit.vim で、このバッファは Vim script として独立している。そう。quickrun.vim を使えばよい。このバッファはファイルとして存在していないが、quickrun.vim であれば実行可能だ。

こちらの方法で print デバッグしたい場合は、echo コマンドを使えば OK だ。わかりやすい。

ちなみにこれらの echo の行は、Bash としての実行結果には一切影響を及ぼさないので、そのまま投稿することも可能だが、当然実行時間は長くなってしまうので注意が必要だ。

最後に

Vim script で AtCoder に参加する方法について紹介した。
今回の例では出てこなかったが、当然 main 以外の関数を定義することもできる。
また、よくある操作については vital.vim のコードが参考になる。Data.ListData.StringMath 辺りには便利関数が揃っている。NYSL なので、コピペして使ってもまったく問題ない。是非活用して欲しい。
Enjoy Programming!

*1:軽く調べてみたところ、練習用コンテストの Vim のバージョンは 7.3.429 だった。Ubuntu 12.04 の Vim がこのバージョンなので、この環境を使っている可能性が高い。コンテストによっては環境が異なる可能性があるので注意。

*2:一応別の方法で回避はできる。