Vimは万能 (準備編)

この記事は, Acompany Advent Calendar 2022 13日目 の記事です.

はじめに

皆様は普段どのエディタを使用していますか?この質問に100人中150人くらいはVimと答えると思います.Vimはメモ帳より高機能でサクラエディタより拡張性が高く,Emacsより使いやすくAtomよりかっこよくてVSCodeより短いという素晴らしいエディタです.さらにVimはエディタとして素晴らしいだけでなく,あらゆる事象を何でもできる,つまり万能です.
ただ,多くの読者はVimのエディタとしての一面しか見ていないと思うので,Vimがいかにして万能かをご存じないと思います.そこで本記事では,Vimが万能であることを証明し,Vimの素晴らしさを皆様にお伝えしたいと思います.
ただし,証明パートの内容が少し重くVim未履修者は読み進めることが困難であることが想定されるため,まずは準備編と題して,証明に必要なVimの知識だけを本記事で触れていきます.実際に証明を行う証明編はこちらの記事になります.既にVimの基礎知識を有している方は本記事を飛ばして証明編にお進みください.

万能であるということ

「万能」を証明するためには,まず「万能」がどのような状態であるかを定義する必要があります.実は計算理論上では万能であることの1つの指標が存在します.それがチューリング完全です.
チューリング完全チューリングマシンと呼ばれる抽象的な計算モデルの性質の1つで,あらゆるチューリングマシンを再現できるようなチューリングマシン(万能チューリングマシン)がチューリング完全と呼ばれます.誇張して言い換えると,チューリング完全なシステムはマシン上において万能であると言えます.そのため,今回Vimが万能であることを示すにあたって,Vimチューリング完全であることを示すことで万能であることを証明します.

Vim

前述したように,証明を始める前にまずVimを扱う上での簡単な知識を本章で紹介します.Vimはメモ帳などの一般的なエディタとは異なり,カーソルを動かしたり文字を入力するのにもVimコマンドと呼ばれるコマンドを実行する必要があります.ただ,Vimコマンドは他のエディタでいうショートカットのようなイメージなので,一度知ってしまえば対して難しくはないです.以下ではいくつかのVimコマンドおよび,証明に必要なVimの機能だけを簡単に紹介していきます.
なお,Exコマンドと呼ばれる特殊なコマンドを使用すると,証明は非常に容易でチューリング完全であることも自明になってしまうため基本使用しないものとします.つまり,単純なVimコマンドのチューリング完全性を証明します.

表記上の注意

EscキーやCtrl+Aなどを押下して得られる特殊な文字は本記事上では正常に表示されないため,以下の表記を用いることとします.

Esc <Esc>
Ctrl+* <C-*>
Enter <CR>もしくは単に改行

Vim起動オプション

-s {scriptin}

Vimの起動時に,{scriptin}に書かれた文字を全てユーザーがタイプしたものとして解釈して実行します.例えば,in.vimiHello<Esc>ZZと書いてあるとします.これは「"Hello"と入力してファイルを保存して終了する」時に人力でタイプする文字列です(各コマンドの説明は後述).このファイルを以下のように指定してtext.vimを開くと,文字列がコマンドとして機械的に実行され,"Hello"と書かれたtext.txtが保存されてVimが終了します.

vim -s in.vim text.txt

このオプションを使うことで,Vimコマンドをまるでプログラミング言語かのように扱うことができます.証明の肝となるオプションです.

-u NONE

プラグインなどユーザ独自の設定を読み込まないでVimを開きます.
Vimは拡張性が高く,多くの使用者は自分に合うようにカスタマイズをしていると思います.ただ,証明用途では環境ごとに動作が変わってしまう恐れがあるのは非常に不便であるため,このオプションの使用を推奨します.

Vimコマンド

ZZ

ファイルを保存して終了します.
保存や終了は個別のコマンドとしても存在します.私自身普段はそちらのコマンドを使用していますが,証明用途ではわざわざ保存と終了を分ける意味もないためここではZZのみの紹介とします.

h,j,k,l

カーソルを1移動します.hは左,jは下,kは上,lは右に移動します.

w,b

カーソルを単語単位で移動します.wは次の単語の先頭へ移動します.bはカーソルが単語の途中であればその単語の先頭,先頭であれば前の単語の先頭へ移動します.

0,$,gg,G

カーソルを先頭や末尾などへ大きく移動します.0は行頭,$は行末,ggは1行目の行頭,Gは最終行の行頭へ移動します.

yl,yw,Y,y$

文字列をヤンク(コピー)します.ylは1文字,ywは1単語,Yは1行,y$はカーソル位置から行末までをヤンクします.ヤンクした文字列は後に説明する無名レジスタ"に保存されます.

x,dw,dd,D

文字列を削除します.xは1文字,dwは1単語,ddは1行,Dはカーソル位置から行末までを削除します.削除した文字列は後に説明する無名レジスタ"に保存されます.

p

直前にヤンク,もしくは削除した文字列をペースト(貼り付け)します.

i,a,o

挿入モードに切り替えます.挿入モードは入力した文字がそのままファイルに書き込まれるようなモードで,一般的なエディタの状態とほぼ同等です.iはカーソルの位置のまま,aはカーソルの右,oは次の行を追加してその行の先頭で挿入モードに切り替わります.
挿入モードは<Esc>を押下することでノーマルモード(元の状態)に戻ることができます.

v,V

ヴィジュアルモードに切り替えます.ヴィジュアルモードは文字列を選択するためのモードで,特定の範囲の文字列をまとめてヤンクしたり削除することができます.vは文字単位ヴィジュアルモードになり,押下時のカーソル地点から移動後のカーソルまでの範囲が選択されます.Vは行単位ヴィジュアルモードで,押下時のカーソルの行から移動後のカーソルの行までの範囲が選択されます.
適切な範囲を選択した後に,yでヤンク,dで削除がされてノーマルモードに戻ります.また,<Esc>を押下することで何もせずにノーマルモードに戻ることもできます.

/{pattern}<CR>

ファイル内から{pattern}にマッチする文字列を検索します.検索時はカーソルの位置から先で一番最初にマッチする文字列にジャンプします(前方検索).さらに,nでその先のマッチする文字列へジャンプ,Nでその前のマッチする文字列へジャンプします.

その他の機能

繰り返し

{繰り返し回数}{command}と押下することで,あらゆるコマンドを任意の回数繰り返すことができます.例えば,5xとすると1文字削除が5回,つまり5文字削除されます.123ddとすれば123行削除され,123行分の文字列が無名レジスタに保存されます.

レジスタ

レジスタはいわゆる保存領域です.Vimは様々な種類のレジスタを持っており,用途に応じて様々なものを保存しています.証明には無名レジスタ"と名前付きレジスタ[a-z]を用います.無名レジスタは一時的なレジスタで直前にヤンク・削除したものを保存します.何も指定しないでヤンク・削除した場合は無名レジスタが暗黙的に指定されてると考えてよいでしょう.対して名前付きレジスタはユーザが自由に指定して保存できるレジスタです.一時的ではなく長期的にレジスタに保存したいものがある場合に有効です.ただし,名前付きレジスタはアルファベット小文字[a-z]しかないため,一度に最大26種類しか使用できません.
レジスタへの保存は証明では2種類使用します.1つはレジスタを指定したヤンク・削除です.これは"{レジスタ}{ヤンク・削除}で実現できます.もう一つは直接レジスタに保存する方法です.これはq{レジスタ}{保存する文字列}qとして実現できます.

マクロ

@{レジスタ}と押下すると,{レジスタ}に保存された内容をコマンドとして実行します.例えばレジスタaに文字列a0<ESC>が保存されているとします.ここで@aと押下すると,レジスタaの文字列a0<ESC>が順にコマンドとして実行されて0が入力されます.このように,長いコマンドであってもあらかじめレジスタに保存しておくことで,簡単にかつ何回でもそのコマンドを呼び出すことができます.

使ってみる

さて,以上で証明に必要なVImの機能についての紹介が終わりました.早速次の記事で証明といきたいところですが,実際に使って慣れないと読むのに苦労することが想像できます.そのため,まずは本章にて紹介した機能を使って慣らしていこうと思います.
使ってみる環境はAtcoderとします.AtCoder競技プログラミングを行えるサイトで,なんと選択言語にVimが存在します.Vimの実行コマンドは下記です(参考:Language Test 202001 - AtCoder).

cat - > /tmp/out; TERM=dumb vim -N -u NONE -i NONE -s {dirname}/{basename} /tmp/out > /dev/null 2>&1; cat /tmp/out

紹介していないオプションも含まれていますが,基本は入力ファイルをVimで開き,-sオプションを使用して提出したコード(コマンド列)を実行するだけです.ファイルを保存して終了する必要があることには注意しましょう

ABC254_A

問題概要

3桁の整数が与えられる.下2桁を出力せよ.

提出コード
xZZ
解説

入力を文字列と捉えると,出力は入力の1文字を削除して2,3文字目を残したもので良いです.Vimxで1文字削除ができるのでこれだけで終わりです.最後に保存終了(ZZ)を忘れないようにしましょう.

ABC198_A

問題概要

整数$N$を1以上の2つの整数に分割するのは何通りか.

提出コード
<C-x>ZZ
解説

答えは$N-1$通りです.Vim<C-x>でデクリメントができるのでこれだけで終わりです.

ABC247_A

問題概要

4桁のbit列が与えられる.右シフトせよ.

提出コード
i0<ESC>$xZZ
解説

次の2つの操作が実現できれば良いです.

  1. 先頭に$0$を追加する
  2. 末尾の文字を削除する

先頭に$0$を追加する処理はi0<ESC>でできます.iで挿入モードに切り替え,$0$を入力,<ESC>ノーマルモードに戻して終了です.末尾の文字を削除する処理は$xでできます.$でカーソルを末尾に移動し,xで1文字削除をします.以上の2つの処理を順に書けば解けました.

ABC107_A

問題概要

長さ$N$の列の前から$i$番目の要素は後ろから何番目か.

提出コード
wD0@"<C-X><C-A>ZZ
解説

答えは$N-i+1$です.Vimコマンドだけでこの計算を行うには少し工夫が必要です.1例として次の方法が考えられます.

  1. $i$をレジスタに保存して削除する
  2. $N$を$i$回デクリメントする
  3. $N$を1回インクリメントする

$i$をレジスタに保存して削除する操作はwDでできます.この際,$i$はレジスタ"に保存されます.$N$を$i$回デクリメントする操作は0@"<C-X>でできます.@"としてマクロを実行するとレジスタ"の中身が実行される,つまり$i$が押下されたことになります.このまま<C-X>をするとVimの繰り返しが発生し,$i$回<C-X>が実行されます.$N$を1回インクリメントする操作は<C-A>です.

おわりに

本記事ではVimが万能であること,チューリング完全であることを証明するための準備として,Vimの基礎的な機能,簡単な使用例を紹介しました.ただとりあえずそんな話は置いておいて,Vim非使用者は今日からVimを使い始めましょう.使うための材料は揃っているはずです.まずはリポジトリ上の.vscodeディレクトリを殲滅することから始めましょう.

参考

vim-jp.org