Leverage Copy

メモの公開場所

Vimmer3年目が読む実践Vim

実践Vimの復習。

新卒でVimを覚えて、VSCodeVim入りのVSCodeに一時期浮気して、 NeoVimに引っ越し&coc.nvimの存在を知ってからはNeoVimにどっぷり。 いよいよ3年ぐらい経とうとしているものの、まだまだ実践できていなものも多いので、Vimの運用を見直すべく復習してみる。

完全に自分用だけど、もしかしたら思考がシンクロしがちな人がいたら刺さる部分があるかも。

雑に以下のカテゴリに分けてみた。

  • レジスタ
  • マクロ
  • Exコマンド
  • パターン、マッチ、vimgrep
  • その他

レジスタ

レジスタは単なるテキストの置き場所ではあるものの、他の機能やコマンドと併用することで柔軟な操作が可能になる。

レジスタの種類

名前付きレジスタ

小文字アルファベット1つ1つが名前付きレジスタのため、26種類存在する。 大文字アルファベットを使うと、指定したレジスタに「追記」される。

" は無名レジスタ

レジスタを指定しないと、この無名レジスタを使う。

0 はヤンクレジスタ

y{motion} という使い方がされると、またその時のみ、無名レジスタに加えて、ヤンクレジスタにも内容がコピーされる。 ヤンクレジスタは無名レジスタに比べて変化しにくい、と言える。

クリップボードレジスタ+ or *

厳密にはシステムごとに違うっぽいが、少なくとも自分が触ってきたMacOSUbuntuに限っては + で問題なさそう。

_ブラックホールレジスタ

/dev/null みたいなイメージ。 "_d{motion} とすると、無名レジスタを汚さずに済む。

Expressionレジスタ =

これはちょっと使い方が特殊なので、実用例は後述。

その他のレジスタ(読み取り専用レジスタ

特に検索パターンを覚えておくと便利。

  • %: 現在のファイルのファイル名
  • #: 代替ファイルのファイル名?
  • .: 直前に挿入されたテキスト
  • :: 直前に実行されたExコマンド
  • /: 直前に検索された検索パターン

レジスタの使い方

コマンドモードもしくは挿入モードで <C-r> と入力してからレジスタを指定する

今回の復習で最重要な手法の1つ。

<C-r>" とすると通常のヤンクと同じだが、カーソルの後に挿入されるというのが明白なのが利点だったりする。 ヤンクしたものを貼り付けたい場合は、タイプのしやすさは <C-r>0 のほうがいいかも。

Expressionレジスタの呼び出し方

= はExpressionレジスタで、 <C-r>= とすると挿入モードからプロンプトが出せる。 この間は「コマンドラインモード」に分類されるらしい。 Enterで入力を確定させると、バッファに結果が挿入される。

削除・ヤンク・プットコマンドはレジスタを使う

"ayiw"ap などがノーマルモードでの使い方。 Exコマンドでもレジスタは指定できる。

:delete c は、レジスタ c に現在のカーソル行をカットする。
:put c は、レジスタ c の内容をカーソル行の直下に貼り付ける。
:yank c も似たような感じになる。

Vimスクリプトを書く際には覚えておきたい。

レジスタを使ってビジュアルな選択範囲を置換

ビジュアルモードで p とすると、指定されたレジスタの内容で選択範囲が置き換えられる。 レジスタにはビジュアル範囲が入ることになる。 覚えておくと割と効率的に使えるかも。

レジスタの内容を確認する方法

:reg {registers} で指定したレジスタの内容を表示する。 引数なしだと、設定されているすべてのレジスタが閲覧できる。

通常ビジュアルモード、レジスタ、パターン検索の併せ技から学ぶ

"uyi[/\V<C-r>u<CR> こうすることで、レジスタuにヤンクしてその内容でパターン検索、という形にできる。

レジスタを介するこの手法は、汎用性が高そう。

置換文字列でVim script式評価を使う

まず、Vim scriptでは、レジスタの内容を @{register} で参照できる。

置換文字列においては \= のあとにVim scriptを書くことでそれが評価される。 :%s//\=@0/g は「直前の検索パターンをヤンクレジスタの内容で置き換えろ」という命令になる。

レジスタVim scriptで書き換える

:let @a = 'Practical Vim'レジスタaを設定している。


マクロ

マクロもあまり使ってこなかったが、「レジスタに記録したノーマルモードのコマンドを再生する」と考えると、割とフランクに使えるかもしれない。 とにかく数をこなさないとこういうのはできるようにならないので、ちょっとでも繰り返しが多そうなタスクに出会ったら意識したい。

マクロの記録と実行

ノーマルモードq{register} と入力するとマクロの記録が開始され、再度 q とタイプするとそこで終了する。 シーケンスはレジスタに入るため :reg {register} で確認できる。

そして実行は、ノーマルモード@{register} とすればよい。 また、 @@ とすると、直前に呼び出されたマクロを繰り返す。

以下はマクロの考え方のコツ(書籍より引用)。

マクロを「直列」に実行するテクニックは脆い。 クリスマスツリー用の安い電球と同じで、簡単に壊れてしまう。 マクロを「並列」に実行するテクニックはもっとエラーに強い。

マクロを記録するというのは、仕事の一単位を行うようにロボットをプログラミングするようなものだ。

マクロを並列に実行するのは、ベルトコンベアを全く使わずにやるようなものだ。 ベルトコンベアを使うのではなく、たくさんのロボットを用意するのだ。 これらのロボットはすべて、ある1つのシンプルな作業だけを行うようにプログラムされている。 各ロボットには、するべき仕事が1つだけ割り当てられる。 うまくいったら...それはよかった。 失敗したら...まあ、問題はない。

黄金律

これも書籍からそのまま引用。

マクロを記録するときには、すべてのコマンドは必ず繰り返し可能であるようにしよう。

以下の3点もとても重要。

  • カーソルの位置の正規化
    • ^$0 といったモーションを使うと、どの列にいてもまずはカーソル位置を正規化できる
  • 繰り返し可能なモーションでターゲットを撃破しよう
  • モーションに失敗したら処理は中断する
    • 安全装置!

最後はうまく利用するととても強力(※デフォルトでは、モーションが失敗すると、Vimはビープ音を鳴らしてくれる)。 マクロがその段階でストップしてくれる、というのはうまく利用できる。

10@a とするとマクロは10回実行される: この数字が適当に決められても問題のないようにマクロを組むと良い。 冪等性は大事。 特に、アットマークと同じ位置にある 22 という数字は試しに使ってみる。 同じ理由で、マクロも普段使いする際は q レジスタを使えば良さそう。

:normal @a でマクロを並列実行する

これを行指向のビジュアルモードで行うと、選択範囲の各行に対してマクロを実行するようにVimに伝える。 というか、「マクロもノーマルコマンドの一種である」ということを認識するほうが大事かもしれない。

並列実行は強烈だが、直列のほうが有効な場合もありうる。 とはいえ、1行に対する処理に集中できるのであればとりあえずは気楽なので、まずは1行に対するマクロを考えてみる。

qA のように大文字でレジスタを指定するとマクロを追記できる

レジスタの使い方を考えると納得できる。

:argdo normal @a で複数のファイルにマクロを適用する

かなり応用的な気がするので、マクロに大分自信がついてからでいい気がする。

イテレータを評価してリスト中の要素に番号をつける

各行の行頭に数値付きのマークをつけるのは、マクロ典型と言えるかもしれない。

最初に :let i = 1 として、以降マクロ中で :let i += 1 とし、挿入モードで <C-r>=i とすると、いい感じにインクリメントしつつ値をバッファに入力できる。

というより、マクロ中ではExコマンドの実行や、挿入モードからEscで抜ける、などもできることに注目したい。 また、範囲指定して :normal @q として並列実行しても、これも結局はマクロは一つずつ上から実行されるので、ちゃんと値はインクリメントされる。

マクロの内容をプレーンテキストのように編集する

基本的には、マクロはレジスタに文字列を貼り付けたようなものなので、それを操作すれば良い。

マクロを吐き出すには :put q のようなputコマンドが便利。 これを編集して、また再びレジスタにヤンクすれば良い。 ヤンクするときは 0"qy$ のようにすると、行末の改行は含めないので覚えておきたい。

とはいえ、マクロ内には特殊文字が含まれがちなので、単純にappendする場合は qQ とかのほうが良さそう。

Vimレジスタは、テキストの文字列を保持するコンテナに過ぎない

以下のように、関数を使った汎用的なマクロも組めるかもしれない。

:let @a=substitute(@a, '\~', 'vU', 'g')

Vim scriptの関数は :h function-list で調べられる。


Exコマンド

プラグインのコマンドはよく使っていたが、Vim組み込みのコマンドはあまり使いこなせていなかった気がする。 argdo などの複数ファイルに渡って変更を加える操作は状況によっては強力に使えると思うので、一通り頭に入れておきたい。 組み込みのコマンドを頭に入れておくことは、Vim scriptで込み入ったことをしたいときにも活きる、と思う。

Vimコマンドラインモード(Exコマンド)

「どういうときにコマンドラインモードになるのか」をまずは理解する。

コロンを打ったとき、スラッシュで検索するとき、Expressionレジスタにアクセスするときは、すべてコマンドラインモードとなる。

Exコマンドを、アドレスで行範囲を指定する

:{start},{end} で指定できる。

基本は行番号を指定することになるが、いくつか特殊なものがある。

  • :/<html>/+1,/<\/html>/-1: 合致するタグに挟まれる部分を指定する、オフセットの構文を利用している
  • %: ファイル全体を示すが、これは :1,$ と等価であることを意識したい

copyコマンド

:6copy. とすると、「6行目を現在行にコピー」となる

ビジュアルモードで範囲指定してから :$ とタイプすると、選択範囲が末尾に貼られる。 覚えておくと、案外普段から使えるかもしれない。

:t でも等価らしいので、手打ちするときはこちらのほうが早い。 また、レジスタを介さないので、クリーンでもある。

moveコマンド

:move もしくは :m でよい。

範囲や移動先の指定方法はcopyと同じ。

Exコマンドの繰り返し

@: で前回のExコマンドを繰り返せる。

これはマクロの一種と考えて良さそう。

normalコマンド

normalコマンドが何を指すのか、というのは個人的によく忘れるので、具体例で覚えてしまうのが良い。

:'<'>normal . は「選択範囲にドットコマンド」となる。
:%normal A; は「ファイルのすべての行の行末にセミコロンを追加」となる。

normalコマンドのルールは覚えておく。

  • 挿入モードで終わるコマンドでも、自動的にノーマルモードに復帰する
  • 指定したノーマルモードコマンドを各行に対して実行する前に、カーソルをその行の先頭に移動する
    • :%normal i// は、カーソル位置を問わず先頭に追加するようになる

ノーマルコマンドは :normal . or :normal @q が最強

逆に言うと、これらのノーマルコマンドが適用できるように、ドットコマンドやマクロを組むことを考えるように意識したい。

コマンドモードで <C-d> と打つと可能な候補が閲覧できる

それ用のプラグインを入れてしまっているのでそんなに使わないかもしれないが、バニラなVimを使う際には役に立つかもしれない。

コマンドモードで <C-r><C-w> とすると、モードに入る前のカーソル上の単語をコマンドに入力できる

<C-r><C-a> とすると、いわゆるwordではなくWORDを入力できる。 覚えづらいがいつか役に立つかも?

q: でコマンドをVimライクに編集する

便利である一方で、案外あまり使う機会は無いのかもしれない。

q/ とすると検索コマンドを編集できるが、こちらのほうが複雑な正規表現などを入力したいときに便利になるかもしれない。

また、普通にコマンドラインモードに入った後に <C-f> とすると、途中からコマンド編集モードに切り替えられる。 ただし、これは自分の環境ではポップアップを出すプラグインのため、ちょっと使いづらいかもしれない。

コマンドラインでは % は現在編集中のファイル名

拡張子だけを取り出したりもできるので :h filename-modifiers というヘルプも覚えておく。

%:h でアクティブなバッファのディレクトリ情報だけを取得する

以下のようなコマンドモードのキーマップを用意すると %% で展開できるようになるっぽい。

cnoremap <expr> %% getcmdtype() == ':' ? expand('%:h').'/' : '%%'

バッファの内容を標準入力・標準出力に接続する

:read !{cmd}:write {cmd} のこと。

前者は「コマンドの出力をバッファに吐き出す」、後者は「標準入力としてバッファの内容を使用する」となる。

後者が場合によっては強力かも。 :write !sh とすると、バッファの各行をシェルに実行させられる。

readとwriteの目的語がなんなのかを意識しないと、わからなくなってしまう。 両者とも目的語はシェルコマンドであることに注意する(シェルコマンドの結果をreadする、シェルコマンドに書き込む(≒標準入力に渡す))。

外部コマンドを介してバッファの内容をフィルタリングする

これはビジュアルモードと :!{cmd} というシェルコマンドの組合せで行われる。

選択範囲を標準入力に渡しコマンドを実行、コマンドの出力でビジュアル範囲を上書きする。 「指定した {cmd} によって [range] がフィルタリングされる」とも考えられる。

:bufdo コマンド

:ls で列挙されるすべてのバッファに対してExコマンドを実行できる。 おぼえておきたいが :argdo の方を理解してマスターしたい。

:args コマンド

引数リストを出力するコマンド。

引数リストという概念から大事。 引数リストは、vimコマンドの起動時に引数として渡されたファイルのリストを表す。

:args コマンドはそれ単体だと引数リストの出力だが、引数を渡して実行すると、渡した引数で引数リストが再設定される。 基本的には、存在する複数のファイル名をglob形式で指定することになる。 もちろん、直接指定するのもあり。

また、シェルのようにバッククォートでの出力結果を渡すこともできる。

" .chaptersにファイル名が列挙されている場合、引数リストがいい感じに設定される
:args `cat .chapters`

argsコマンドを実行すると新たにファイルがバッファにオープンされるが、 すでに開かれたバッファを閉じる、ということにはならない。 ただ、引数リストはちゃんと更新されている(通常 :ls コマンドとは異なった内容となる)。

:args ... のあとに所望のファイル群に :argdo したりする。 :argdo の一番簡単な例は :argdo write でバッファを一括保存すること。

:edit! でバッファの内容を捨てファイルをディスクから読み直す

リロードの手段としてちゃんと覚えておきたい。

:qall!:wall コマンド

見ての通りのコマンド。 全部に対して実行する、というのも危険な場合はあれど覚えておきたい。


パターン、マッチ、vimgrep

まずは、「パターン」と「マッチ」の意味を正しく把握するところから。

パターンといったら、それは検索フィールドに入力する正規表現(もしくはリテラルなテキスト)のことだ。 マッチと言ったら、それはドキュメント内で強調表示される何らかのテキストのことだ。

続いて、vimgrepはVimの検索パターンを流用できることの意味が大きい。 grepperなど非同期で便利なプラグインもあるが、検索がしやすいvimgrepでquickfixリストを更新できるなど、覚えておいて損はないはず。

very magic検索 \v を使うとすべての特殊記号に関する規則を正規化できる

\v すると、アンダースコア、大文字小文字のアルファベット、数字を除く、すべての文字が特殊な意味を持つことになる。

普通に正規表現() などを使おうとすると、すべてエスケープする必要が合って面倒だが、 \v とすることで正規表現を意図したパターンが書きやすくなる。

nomagic検索 \V を使う

very magicの真逆で、こうすると素直に入力した内容そのままでのテキスト検索が出来る。

バックスラッシュ以外は普通の意味を持つことになる。 一般的には、正規表現を検索したければ \v パターンスイッチを使い、テキストそのものを検索したければ \V リテラルスイッチを使う。」

単語境界デリミタを覚える

たとえば /\v<the> とすることで their などの単語をヒットしないようにすることができる。 very magicを使わないと /\<the\> としなければならないことに注意する。

マッチ境界 \zs, \ze を理解する

これは例で理解するほうが良い。

パターンを /Practical Vim とすると素直に「Practical Vim」が強調表示される。 /Practical \zsVim とすると「Practical Vim」というフレーズのVimは強調表示されるが、Vim単体のものは強調表示されない。

他にも、ダブルクォートに囲まれるもののみマッチさせようとすると、以下のようになる。

/\v"\zs[^""]+\ze"

マッチからは除外されていても、パターンのなかでは、ダブルクォートは重要な要素であることに注意が必要。

レジスタ中の文字列をエスケープする

escape(@u, '/\') とすると「レジスタu中のスラッシュ・バックスラッシュをエスケープする」という具合。

検索のマッチの末尾にカーソルをオフセットする

これは何気にとても重要だと思う。

/vim/e のように、 /e を付け加えると、ジャンプする先がマッチの末尾になる。

これによって、 a コマンドから単語に数文字付け加える、ということが可能になる。 これはドットコマンドとうまく組合せられるかもしれない。

この類のものは :h search-offset で調べられる(検索オフセット)。 あとからこれが必要と気づいたときには、 //e とすることで前回のパターンを再利用できる。

f コマンドは大文字にめがけて使うといいかもしれない

fコマンドはもっともっと使いこなしたい。 大文字の存在感は大きいので、使い方として覚えておく。

q/ とすることによってコマンドラインウィンドウを表示できる

q: と同じ要領で編集ができる。 検索コマンドは複雑なので、複雑な検索がやりたいときは思い出すといいかもしれない。

現在のビジュアルな選択範囲の検索

以下のVim scriptで可能なようだが、応用が効きそうなのでそのまま引用しておく。

" xnoremapは「ビジュアルモードには適用されるが、選択モードでは適用されない」というもの
xnoremap * :<C-u>call <SID>VSetSearch()<CR>/<C-R>=@/<CR><CR>
xnoremap # :<C-u>call <SID>VSetSearch()<CR>?<C-R>=@/<CR><CR>

" /レジスタの内容を書き換えている
" gvは直前の選択範囲を再利用する
function! s:VSetSearch()
  let temp = @s
  norm! gv"sy
  let @/ = '/V' . substitute(escape(@s, '/\'), '\n', '\\n', 'g')
  let @s = temp
endfunction

直前の検索パターンを流用する

無意識でやっていることが多いが、改めて再認識する。

置換コマンドが2つのステップで構成されているところがポイントだ。 つまり、パターンを組み立てることと、適切な置換文字列を考えること。 この方法を使えば、いつだって、置換コマンドをこれら2つの作業に分割できる。

visual-starなんかも有効活用できる。 まずvisual-starでパターンを決めてから(マッチを確認し)、 :%s//.../gc とすればよい。

部分マッチを使ってCSVのフィールドを入れ替える

正規表現パターンにおけるマッチのキャプチャの例として知っておく。

/\v^([^,]*),([^,]*),([^,]*)$
:%s//\3,\2,\1

\x の部分が、部分マッチの参照となっている。 Vim scriptの関数だと submatch(x) が同等っぽい。

複数のファイルに置換コマンドを実行する

:argdo %s//Practical/g のようにする。

パターンにマッチするものがなかったときはエラーを吐くが、これを抑制したい場合は、 置換コマンドの部分を %s//Practical/ge のように e フラグを指定すれば良い。

ターゲットのパターンを含んでいるファイルのリストを作成する

vimgrepは同期的であまり高速ではないものの、うまく使えば便利に活用できる例。

Vimに組み込みの検索エンジンを使うので、全く同じパターンを流用できるのがいいところ。 :vimgrep /<C-r>// **/*.txt のようにすると前回の検索パターンでvimgrepできて、ファイルがオープンされる。

./**/*.{ext} のようにすれば、再帰的にすべてのディレクトリを舐めることになる。

vimgrepが返す各マッチはquickfixリストに記録される

vimgrepの後に :copen としてquickfixリストをブラウズできる。

Qargsという、quickfixリストの内容で引数リストを更新するコマンドを追加するプラグインが、実践Vimの作者から提供されている。 これを利用すると、以下のような一連のコマンドが考えられる。

" \zeまでがマッチするようなパターン
/Pragmatic\ze Vim
:vimgrep /<C-r>// **/*.txt
:Qargs
:argdo %s//Practical/g
" updateコマンドは、変更が合ったファイルのみ保存される
:argdo update

また、最後の3つのコマンドはバーティカルバーによって1つにまとめられる。

:Qargs | argdo %s//Practical/g | update

Vimにおけるバーティカルバーは、Unixのシェルにおけるセミコロンと同等。 Vim scriptのワンライナーを書くことは少ないかもしれないが、覚えておきたい。

quickfixリスト

errorformatと呼ばれるものに適合するように出力をパースすることで、quickfixリストが作れるっぽい。

Vimのquickfixリストは、外部ツールをVimで行う作業に組み込むための核となる機能だ。 これは非常にシンプルに、エラーがあると報告されたファイルの「ファイル名・行番号・桁番号(オプション)・メッセージ」で構成されるアドレス群を管理するものだ。

grep/vimgrepなどを使ってプロジェクト全体を検索する

vimgrepコマンドなら、Vimにネイティブな検索エンジンを使って複数ファイルからパターンを検索できることを見てみよう。 これには代償もある。 つまり、vimgrepは他の専用プログラムほどには高速ではない。

Vim:grep コマンドは外部プログラムgrepのラッパー

:grep Waldo *grep -n Waldo * を実行しているのと同じ。 また、 :grep コマンドもquickfixリストを作る。

vimgrepコマンド

:vim[grep][!] /{pattern}/[g][j] {file} ...

gフラグをつけると、1行の複数のマッチを捉える。 jフラグをつけると、最初のマッチへのジャンプをキャンセルできる。 quickfixリストが作りたいだけならつけたほうが良さそう。

file引数は必須。 ファイル名、ワイルドカード、バッククォート式、これらの組合せたものなど、argsコマンドと同じ引数が指定可能。

vimgrepの利点は現在のバッファのマッチを確認することで、パターンを正しく組み立てやすいこと。

:vim /<C-r>// ** とすると現在のパターンで検索してくれる。

注意点として、パターンを空にするのはダメ、ということ。 <C-r>/レジスタから実際のパターン文字列を引っ張ってくる必要がある。

グローバルコマンド

:global コマンドは、ExコマンドのパワーとVimパターンマッチ機能とを結合する。 これを使って、指定したパターンにマッチする各行に、Exコマンドを実行できる。 ドットの公式やマクロと並んで、 :global コマンドは、繰り返し作業を効率的に行うための、Vimの強力なツールなのだ。

[range] global[!] /{pattern}/ [cmd]

rangeを与えないデフォルトでは、ファイル全体 % となる。 他のExコマンド :delete, :substitute, :normal と大きく異なり、ほとんどのデフォルトの範囲はカーソル行 . である。

patternはVimの検索機構なので、空にしておくと現在の検索パターンを自動的に使ってくれる。 その場合は // とだけ入力すれば良い。

cmdはglobal以外のコマンドを指定できる。 デフォルトは :print コマンドが使われるので、表示されるだけで何もしない。 ドキュメント中のテキストを操作するExコマンドを指定すると便利なことが多い。

:global! もしくは :vglobal は、パターンマッチ「していない」行に作用する。

レジスタにTODOアイテムを収集する

yankコマンドを併用して、指定したレジスタにTODOの行をヤンクする。 :argdo とさらに併用すると、複数ファイルからかき集めることもできる。

" 空のマクロを記憶させることでレジスタを消去する
qaq
:g/TODO/yank A

Vim組み込みの :sort コマンドを使う

範囲指定すれば部分的に使える場面は多そう。 ただ、コードフォーマッタに任せたくなることが多そう。

globalコマンドに渡すExコマンドも範囲を受け取れることを理解する

Exコマンドは常にそれ自体が範囲を受け取れることを思い出す。

:g/{pattern}/[range][cmd]

以下のコマンドは、CSSの波括弧内のみをsortするglobalコマンドの応用(難しい)。

:g/{/ .+1,/}/-1 sort

sortの範囲がちょっと難しいが、「現在行(≒各 /{/ のマッチ行)の1つ下の行から、 /}/ のマッチ行の1つ上の行まで」という意味になる。


その他

細々としたトピックだが、かゆいところに手が届きそうなテクニックなど。

単なる単語消去の際は daw がよい

ドットコマンドとの相性も良い。

挿入ノーマルモード

挿入モード時に <C-o> をタイプするとノーマルモードに遷移するが、そこで何かノーマルコマンドを打つとすぐに挿入モードに戻る。 例えば、 <C-o>zz とすると、挿入モードのまま画面を中央に戻すことができる。

普段のコマンドとしては使わないかもしれないが、複合的なコマンドをキーマップするなどすると、うまく適合するケースがあるかもしれない。

文字コードを使って特殊文字を入力

挿入モードで <C-v> とし、その後に文字コードを指定する(みんな文字コードって覚えてるの。。?)

ダイグラフによって特殊文字を挿入

挿入モードでは <C-k>{char1}{char2} とする。 ダイグラフのほうが直感的なため、覚えておくといいかも。

置換モード

ノーマルモードR とタイプすると、置換モードに遷移する。

そもそもこのモードは認識したことがなかったので、ステータスバーが見慣れない色に変化するな、とか思った。 そのままタイプすると、カーソル位置の単語が入力内容で置換され続ける。 使いそうだけど、あまり使いそうにないか。。

ビジュアルモードの再選択

ノーマルモード<gv> とすると、直前に選択した範囲を再選択した状態でビジュアルモードに入れる。

マクロとかで繰り返し作業をする際には出番が多くありそう。

ビジュアルモードの始点・終点をトグルする

ビジュアルモードで o とタイプすると、始点と終点が入れ替わるので、微調整に便利。

通常・行指向・ブロック指向いずれでも直感的な挙動となる。

gugU

それぞれ小文字化と大文字化。

ドットコマンドを便利に使うためには覚えておきたい。

eagea でいい感じに単語の末尾で挿入モードに入る

gee の後ろ方向バージョンだが、 gea と暗記すると案外使えるかもしれない。

wordとWORDの定義を認識する

  • wordは「英文字、数字、アンダースコアが連続したもの」
  • WORDは「非空白文字が連続したもの」

ピリオドとアポストロフィはwordとしてカウントされることも覚えておく。 大幅に移動したいときは W で移動するのもいいかもしれない。

テキストオブジェクトはモーションそのものではない

しかしながら、モーションが適用できる場所ではテキストオブジェクトが適用できる。

存在していないディレクトリにファイルを保存

シェルのmkdirコマンドを使って、以下のようにする。 !mkdir -p %:h これは先程の知識も利用している。


以上。 なんか思ったよりも膨れてしまった。

定数をVueの状態に持たせることを避ける

久しぶりすぎるブログ投稿なので、リハビリがてら小ネタを。。

TL;DR

Vueインスタンスの可変な状態の数はなるべく減らしたいので、 「一度値が決まったらそのコンポーネントが生存する間は不変」というような状態は、定義の仕方に気をつけよう、 という話です。

以降のサンプルコードなどはVue2のOption API + TypeScriptで説明します。

以下のようなコードがあるとします。

const message = '不変な値'

export default Vue.extend({
  data() {
    return {
      message,
      ...
    }
  },
  ...
})

「Viewにレンダリングさせたいものの、その値は最初に設定した定数であるようにしたい」ようなケースです。 本当に決まりきった定数だったら、最初からViewに放り込んでしまえばよいのですが、 アプリケーションの状態に応じて初期値を変えたり、Message Providerのような外部モジュールから取得した値をセットしたい、 という状況は割とあり得るのではないかと思います。

まずい点

ただし、このようにすると、Vueの状態である message は、Vueインスタンス内部から容易に変更できてしまいます。 チーム内の紳士協定によって不変であることを維持するのは大変ですし、また、 Vueインスタンスがそれなりに大きい場合、この message はいつ値が変わるかわからないグローバル変数のように感じられ、 将来コードを読む際に負担になってしまいます。

解決策

この場合は、インスタンス外で定数を定義し、算出プロパティの派生元として利用するのが良いです。

const message = '不変な値'

export default Vue.extend({
  computed: {
    msg() {
      return message
    },
  },
  ...
})

算出プロパティの派生元のデータは、Vueインスタンスdata オプションやVuex Storeのゲッターを用いることが主なため、 こういった用法は盲点になりやすい気がします。

備考1

注意点としては、当然ながら派生元のデータはVueに対してリアクティブなデータではないため、 該当する算出プロパティもリアクティブとはならないことでしょうか。 今回は const で定義されたプリミティブな値が派生元なので関係ないですが、そうでない場合は注意が必要になるかもしれません。

また逆に、本当ならばVueの状態として data に持たせなければならないものを、インスタンス外でミュータブルな変数として定義してしまうと、 本来破棄すべきタイミングに破棄されずに残ってしまったり、と余計に状態管理が面倒になるかもしれません。

「Vueインスタンスの状態として持つべきものはなにか?」というのを常に考える必要があると思います。

備考2

TypeScriptに詳しい場合は、ひょっとすると予め厳密にOptionの型を定義しておくことで、 readonly な状態みたいなものが作れるのかもしれないです。 ただ、できるにしてもかなり手間がかかりそうですし、overkillな印象です。

出典

みんなのVue.jsという書籍の「第2章: 状態管理パターン」という章に説明されている手法の紹介でした。 この書籍はやわらかいタイトルとは裏腹に、確かに実際の開発業務で役に立つ知識がぎゅっと詰まっている印象でした。 それでいて、文章やサンプルなどは平易なものがほとんどのため、非常に読みやすかったです。

おわりに

最近は、如何にしてVueのコードを「スリムに」できるか、に腐心しているような気がします。

近況報告(2021-05-17)

随想です。

今年の2月中旬頃に転職活動を始めまして、4月末を最終出社日として新卒から3年間お世話になった会社を退職しました。 これまで一緒に働いてくださった皆様には感謝申し上げます。 次の会社の入社日は6月1日でして、それまでは休息期間になります。

それに伴って、東京から静岡に引っ越しました。 人事の方からは「リモートワークが中心になるのと、東京にも出社するスペースがあるので東京に残ることも可能ですよ」と提案されたのですが、静岡の物件と家賃を調べてたら「今の家賃から数万下げても、広さを倍にできるな...」とか考えてしまい、引っ越しを考え始めました。 転職後も在宅勤務が中心になるのは間違いなさそうで、そうなるともっと自宅環境に投資したいという気持ちが強かったのも大きいです。

あと、最近は瀬戸口みづき先生の「ローカル女子の遠吠え」にハマってて、漫画の舞台の静岡に大分魅せられていることもあり、当初からかなり前向きに検討していました。 漫画のリンク張っておきます。 ニコニコ漫画とかでもためし読み出来る*1ので、面白かったら買って続きも読みましょう! ついでに、同著者の「めんつゆひとり飯」も面白いので買おう!

結局引っ越しをしまして、2週間転居先で過ごしてみましたが、今の所大変快適です。 駅の近くなので徒歩でなんとかなっていますが、近辺に行ってみたい観光地が多いので、自動車がほしいなーなんて思い始めてます*2

さて、推し漫画の紹介も出来たので本投稿の目的の大部分も果たしてしまいました。 とはいえ、「転職+引っ越し*3」というそこそこな規模感のライフイベントを機に、色々と日々の習慣を見直して実践することにしたので、そのあたりを気ままに紹介してみたいと思います。


睡眠と食事

とあるきっかけ*4で、以下の2冊の本を読んでみました。

スタンフォード式 疲れない体

スタンフォード式 疲れない体

スタンフォード式 最高の睡眠

スタンフォード式 最高の睡眠

スタンフォードって世界トップの教育・研究機関っていうイメージだったんですが、スポーツの世界でも最高峰の集団なのは本を読んで知りました。 2冊とも、著者は実際にスタンフォードに在籍する研究者で、それぞれスタンフォードの一流のアスリートの体調を管理したり、睡眠の研究をしていたりと、情報源としては信頼できるのかなと思いました。

あんまり本の内容をギチギチに実践しても疲れてしまうので、一読して印象に残った部分を生活に取り入れることにしました(太字は特に効果を感じたもの)。

  • 腹「圧」呼吸(IAP呼吸法)の実践。
    • 普段から行うことで、背筋が伸びたり、全体的に体のバランスが良くなる、らしい。
    • 思い出したときや休憩中にやるようにしている。
  • 座り過ぎは良くないので30分ごとに立ち上がる。
    • 肩甲骨を開く運動とかも紹介されていたので、立ち上がるたびにそれもやる。
    • 自動昇降機能がついたデスクを持った人はやりやすそう。
  • 甘いものは食べすぎない。
    • 糖質制限否定派だったけど、どうも糖分は普段の食事で十分らしい。なので、タンパク質やビタミンを取れるような間食を意識しだした。
  • 寝る時間を一定にする(「入眠定時」を設ける)。
    • なんかいい感じに体を正常に保つホルモンは、ある睡眠リズムを作ったら一定の時間帯でしか分泌出来ない云々。ということで、安定した睡眠には不可欠なんだと思う。
  • 入眠の90分前に入浴することで入眠を促す。
    • 体の深部体温と表面の体温の差が縮まることで入眠が促されるとかなんとか。
    • 「入眠定時」と合わせることで、布団に入ったら30分ぐらいで寝付けるようになったので、効果の高さを感じた。
  • 睡眠前は刺激のあることはやらない。
    • 難しい本を読んだり、ゲームをやったり、スマホブラウジングしたりはNG。
    • 以前やってた「寝る前にじゃんたま1半荘だけ」とかは最悪だった(というか1半荘で済まないことが多い)。
  • いい睡眠はいい覚醒から。
    • ご飯をよくかんだり、栄養バランスの取れた食事をとったり、腹圧呼吸 or 腹式呼吸をしたり。
    • 「20分以上の」昼寝は良くないらしい(そんだけ長く昼寝しちゃうってことは、睡眠負債溜まってるから普段の睡眠を見直せ、とのこと)。

また、食事に関しては、BASE BREADを注文してみました。

正直、休暇中は積極的に食べようとは思いませんでしたが、働き始めたら昼食、あるいは間食には続けられそうだと思いました。 味はプレーン、カレー、メープル、チョコレートを試しましたが、個人的にはメープルとチョコレートだけを継続したいかも。

N度目の正直の「ポモドーロテクニック

最近、ものすごく集中力が落ちていると感じることが増えました。 本を読むにしても考え事をするにしても、些細な他の用事が頭に浮かんでくるとすぐそちらに目移りしてしまって、本来やるべき作業を中断してしまう、ということが多くなってしまいました。 前職でビジネス側の問い合わせを意識しすぎてSlackを注視し、時間のかかる作業を後回しにしてしまっていたのも、根本的には同じ問題だったのかもしれません。

下の本のKindle版がちょっと前まで500円ぐらいなのを見かけたので、買って読んでみました。

ポモドーロテクニックって、今まではWebで見かけた断片的な情報しか知らなかったんですが、ちゃんとシステマチックにやろうとすると結構難しいんですね。 完全に身につけようとすると意識することも多いし、練習もそれなりに必要そうです。

とはいえ、改めて実践してみたところ「タイマーが動いている25分間はがっちり集中できる」ということがわかったので、とりあえずしばらくはタイマーの奴隷になってみようと思いました。 加えて、5分間の休憩は、前述の疲れにくい体を作るための「30分毎に立ち上がる」にマッチしていていい感じです。

あとは「タスクの見積もりの練習になるかも」というのを少し感じました。 ソフトウェア開発をしていると見積もりは日々発生する作業だと思いますが、自分は未だに苦手意識が拭いきれていません。 単純な力不足もあるかもしれませんが、25分1ポモドーロというタイムボクシングと、「あるタスクをこなすのに何ポモドーロ要したか?」という振り返りの積み重ねは、案外大事なのかもしれないと感じています。

やや余談ですが、ポモドーロテクニックって個人だけじゃなくてチーム単位の手法も含んでいるんですね。 というか、上記の本の半分以上はチームで導入する場合の説明でした。 これに関しては、「アジャイルですら一定の障壁があって難しいのに、厳しくない?」と思い、一旦忘れることにしました。 あくまでも「自分が集中できること」を第一目標として続けていきます。

振り返りを習慣づける

振り返りの重要さは頭ではわかっていたつもりなのですが、今まで習慣化出来ずにいました。 転職活動にあたっては、まずは履歴書や職務経歴書を書いたりすると思いますが、私としては正直この工程が一番疲れてしまいました。 普段から振り返りの習慣が出来ている人は、こういった作業は難なくこなせるんだろうな、とか書類を書きながら考えていました。 別に転職を考えていなくても、細かく振り返りをすることで日々の行動の修正がしやすくなって停滞する時間を短くできるとか、基本的に良い効果がもたらされると思います。

「ところで振り返りの手法ってどういうのがあるんだっけ?」と軽く調べてみたら、たくさん出てきました。 というか多すぎる。

【2020年度版】振り返りフレームワーク手法 まとめ

一番はじめにあるKPTは、前職でも期ごとにチームでやったりしました。 ただ、どうも調べてみるとKPTにはやや否定的な意見もあると知りました。

KPTとYWTの違いは?~KPTがうまくいかない理由と、YWTの特性を考える

なるほど、読んでいると前職で振り返りのファシリテータをやっていた手前、反省点が思い起こされて耳が痛いです。 もっと深堀して勉強したくもなりますが、とりあえず今は「自分個人の振り返りの習慣をつけること」なので、実践と継続のしやすさを考えて「YWT」のフレームワークを採用してみようと思いました。

「やったこと、わかったこと、つぎやること」で「YWT」だそうです。 実際にやってみると、「だいぶフランクに出来るんだな」と感じました。 結果として「一定のルールに則った日記」みたいなものが出来上がります。 自分の場合、GitHubにprivateリポジトリを作ってそこに毎日pushする感じで進めています。 1週間毎に見返してみると「結構色々なことをやって、何かしらの知識と経験は得られたんだな」とかがわかるので、多少は精神衛生的に良いです。

デッドバグとプランクそれぞれ30秒×5回

今のところ、体重とか体脂肪率が危険域に入っているとかは全く無いんですが、それだけにここ最近の下腹部のでっぱりが非常に気になるようになりました。 去年の2月頃に、以前より通っていたジムにいけなくなって、また、在宅勤務になり運動量も激減してしまいました。

転居先は大分広くなり、ヨガマットを気兼ねなく敷けるようになったので、自宅での運動を習慣づけようと思いました。 今は以下の動画を参考にして、デッドバグとプランクを継続しています。


www.youtube.com


www.youtube.com

とりあえず今は夕食後に毎日やっていますが、仕事が始まった後も続けられるのか?が勝負になりそうです。

今、できていないこと

以前まで出来ていたのに今は出来ていない、けどまた始めたいことです。

競技プログラミング

転職活動を始めたあたりで一旦お休みしました。 転職活動が終了した後も、アプリ開発に時間を割いており、全く取り組めていないです。 仕事が始まっても、最初のうちは信頼を得るためにも仕事にリソースを振りたいので、もうしばらくはできなさそうです。

気のせいかもしれませんが、「集中力の低下は競プロをやらなくなったことも起因しているのでは?」とも思います。 問題を解くのはかなり頭の体操になっていて、1日1問だけ、解説ACになってもいいからそれこそ1ポモドーロだけ考え込むのもいいかも、とか思っています。

ただ、再開するにしても、今はアルゴリズムコンテストよりはマラソンヒューリスティックコンテストに力を入れたいな、と思っています。 あるいは、フィールドを変えてKaggleを始めるのもいいなと。 アルゴリズムコンテストに飽きた、とかではないので、時間が無限ならそちらも当然やりたいですけどね。

ランニング

以前は家の近くに川が流れていて、夜の川沿いが比較的走りやすかったのですが、今はまだいいランニングコースが見つけられていません。 個人的には、ランニングは体の健康というよりは心の健康に必要*5なので、3日に1回ぐらいは習慣にしたいです。


こんなところでしょうか。

なんか気づいたら、いわゆる「健康オタク」と揶揄されそうな生活になってきました。 とはいえ、30歳を超えると日々の体のメンテをしないと周りについていけない、と感じるようになったので必要なことなのだと思います。 20代では許されてきたような安易な行為が、今では翌日以降にペナルティを課してくるようになったと感じています。

あとは、次の職場ではフロントエンド周りでの働きを期待されているようなので、1年目にやっていたVueとかその環境周り(webpackとか)を思い出しつつ、最近の流れを押さえるような学習をしています。

東京に居る先輩や友人に「落ち着いたら飲みに行こう」なんて言われて楽しみにしてたのですが、結局難しい状況になってしまいました。 東京までは新幹線で1時間ぐらいでさっと行けるので、平和になったら遊びに行きたいです。 仕事が始まったら、またリモート飲み会かなんかで情報交換したいなぁ。

*1:https://seiga.nicovideo.jp/comic/27678

*2:その前にペーパードライバーなのをなんとかしないといけないけど。

*3:「人間が変わる方法」の一つとして有名ですよね。

*4:「格ゲーマーささき」さんというYouTuberのこの動画この動画で知って興味を持ったので。

*5:わにとかげぎす」って漫画で、ビルの屋上を上半身裸で走る主人公の心情的な。

online-judge-toolsをVimから呼ぶプラグイン

TL;DR

  • kmykさんのonline-judge-toolsのコマンドを呼ぶ、Vimのラッパーコマンドをプラグインにしました。
  • 自分は普段こんな感じで使っています。
  • 現状、カスタマイズ性は微妙ですが、submitコマンドは多少便利だと思うのでよかったら試してみてください。

はじめに

online-judge-toolsをVimから呼んで楽をする の内容をプラグイン化しました。

https://github.com/maguroguma/vim-oj-helper

READMEでは淡々と仕様について(突貫作業なので抜け漏れは多いながら)書いたつもりですが、 この記事では提案する実際の利用方法についてフランクに書いてみたいと思います。

インストール

Vimの標準のプラグインの作法に則っているため、お使いのプラグインマネージャを使えば、 特に問題なくインストールできるかと思います。私はvim-plug を使っており、以下のようにしてインストールできることを確認しました。

Plug 'maguroguma/vim-oj-helper'

このプラグインは、 oj のサブコマンドである download, test, submit の、Vimのラッパーコマンドを提供します。 oj が実行可能である場合のみコマンド群が定義されるため、 oj への PATH が通っていることを事前に確認してください。

使用方法

ここではABC180に参加しているという想定で説明してみたいと思います。


まずはA問題から解くとして、以下のようにA問題のディレクトリ、ファイルを開いて、 A問題のURLをコメントに書きます。

f:id:maguroguma:20201119012349p:plain
サンプルダウンロードコマンドの実行

ここで :OjDownloadSamples を実行してみましょう。

f:id:maguroguma:20201119012427p:plain
ダウンロード後

実行後、カレントバッファに開いているコードと同じディレクトリ階層に、サンプルケースがダウンロードされています。

ここから問題を解いていきましょう。

f:id:maguroguma:20201119012451p:plain
テストコマンドの実行

コードを書き終わったら、先程ダウンロードしたサンプルでテストを行います。
Pythonで参加しているため :OjLangCommandTest python のように実行してみましょう。

f:id:maguroguma:20201119012535p:plain
テスト後

テストは全て通ったので、提出して問題なさそうです。

f:id:maguroguma:20201119012557p:plain
提出コマンドの実行

カレントバッファで開いているファイルを、その問題に対して提出しましょう。 :OjSubmitCode を実行してみましょう。

f:id:maguroguma:20201119012622p:plain
提出後

これで提出が完了しました。 スクリーンショットでは省いていますが、ブラウザのタブが開いて提出画面にリダイレクトされます。


オプションについて

提出前の確認の有無、あるいは文言のアレンジ

提出はある程度慎重に行いたいものですが、人によっては邪魔かもしれません。 これに関しては、確認の有無と文言をオプションで設定できます。 また、提出可能なコンテストサイトごとに設定でき、デフォルトは以下のように全てのサイトに対して確認が行われます。

" default
let g:oj_helper_submit_confirms = {
    \'atcoder': 'AtCoder: Are you sure you want to submit?',
    \'codeforces': 'Codeforces: Are you sure you want to submit?',
    \'yukicoder': 'yukicoder: Are you sure you want to submit?',
    \'hackerrank': 'HackerRank: Are you sure you want to submit?',
    \}

私の場合、Goの環境違いによりこどふぉでのみオーバーフローで痛い目に会いまくっているので、 こどふぉに関しては提出前に確認をするように設定しています。 また、AtCoderとyukicoderに関しては、確認せずに提出できるようにしています。

" customization
let g:oj_helper_submit_confirms = {
    \'atcoder': '',
    \'codeforces': 'こどふぉだけどオーバーフロー大丈夫?',
    \'yukicoder': '',
    \}

例えばこどふぉの問題に提出しようとすると、以下のような確認がなされます。

f:id:maguroguma:20201119012648p:plain
提出前の確認

他のスクリプト言語コンパイル言語のためのコマンド

デフォルトでいくつかのスクリプト言語のためのコマンドは登録していますが、 必要なものはあとからオプションで追加したり、あるいは上書きできます。

" default
let g:oj_helper_lang_commands = {
      \'go': 'go run',
      \'python': 'python3',
      \'javascript': 'node',
      \}
let g:oj_helper_lang_extensions = {
      \'go': 'go',
      \'python': 'py',
      \'javascript': 'js',
      \}

" customization
let g:oj_helper_lang_commands = {
      \'bash': 'bash',
      \}
let g:oj_helper_lang_extensions = {
      \'bash': 'sh',
      \}

一方でコンパイル言語ですが、、正直競技プログラミングの中でコンパイル作業をしたことがないので、 どういった手段を提供するのがよいのか判断できませんでした。

とりあえず今回は、勝手に以下のような仮定を置いてしまいました。

  1. カレントバッファで開いているファイルを、同じディレクトリ階層にDLしたその問題のサンプルに対してテストしたい。
  2. カレントバッファで開いているファイルと同じディレクトリ階層に、コンパイルされてできた main という実行バイナリが存在している。
  3. :OjExecutableBinTest を実行すると、2のバイナリが1のサンプルに対して実行される。

とりあえず、実行バイナリのファイル名だけはオプションでいじれるようにしました。

" default
let g:oj_helper_executable_binary = 'main'

" customization
let g:oj_helper_executable_binary = 'main.exe'

例えば、Goの例だと以下のスクリーンショットのようになります。

f:id:maguroguma:20201119013534p:plain
バイナリによるテストとディレクトリ構造

他にもいくつかオプションはありますが、これら以外は気にしなくともさほど問題にはならないかなと思います。

おわりに

Vimを使って競技プログラミングを行っている方には、ぜひ使ってみてフィードバックをいただけると大変嬉しいです。 競技プログラミングのメイン言語であるC++に対して使いやすいとはいえないであろう点が懸念ですが。。 しかしながら、ダウンロードと提出コマンドについてはうまくフィットする人も何人かはいるんじゃないかなとも思います。

online-judge-toolsをVimから呼んで楽をする

kmykさんのonline-judge-toolsVimを組み合わせて使い始めてから半年ぐらい経ちました。 それから使い続けて以来、新たに不満は生じず「割と便利な運用なのでは?」と思えるようになってきたので、単純なものではあるものの紹介してみようと思った次第です。

競プロに便利なCUIツールを求めて

とりあえず、以下の2つの課題をなんとかしたいなと思っていました。

1. サンプルのテスト

今となってはちょっと考えられないですが、toolを導入するまでは、サンプルのチェックのために 問題のすべてのサンプルのコピペを繰り返す 、ということをしていました。

デバッグが不要で、一発で通せるぐらい自分にとって易しい問題であればこのコストを受け入れてもいいかもしれません。 しかしながら、コンテストで通すべき問題というのは、ときにはデバッグ出力を何回も確認しながら慎重に実装したり、 WAによってコードの修正とサンプルテストの何回もの繰り返しが余儀なくされるものです。 よって、 自身にとって重要な問題であるほど、このコストは大きくなっていきます。

ですので、 CUIからの一回のコマンドにより、一括で問題ごとのサンプルすべてが検証される」 のが理想的です。

2. コードの提出

tool導入前は、 「エディタのコードをコピーして、問題の提出欄にペーストし、選択言語が正しいことを確認してからボタンを押下する」 という作業をしていました。

サンプルのテストほどではないですが、これもいくつかの手順があり、更に問題になるのは以下のような事項だと思います。

  1. 選択言語を間違えてCEしてしまう(単純に時間の無駄)。
  2. 提出先の問題を間違えてREもしくはWAしてしまう(最終的にペナになる可能性があり最悪)。
  3. ブザービートに失敗する(レアケースといえばレアケースだが、逃した時のショックは大きそう)。
  4. コードのコピペをミスる(Vimだと普通にコピペするとクリップボードに載らない*1)。

よって、 「確実に今自分が目に入れているエディタのコードを、正しい問題・正しい選択言語で提出できる」 のが理想的です。

できるだけシンプルなツールを求めて

一応、atcoder-toolsの存在は知っていたのですが、 一見したところ「C++Pythonといった競プロメジャー言語に寄っているっぽい(他の言語は使えない or 使いづらい?)」 とか「特定のディレクトリ構成が強要されるっぽい(逸脱しようとすると凝ったことをしないとダメそう or 調査は必須)」 という印象を持ち、ちょっと自分の要件には合わないかなぁと思っていました*2

結局、頑張って自分用に自作していたのですが*3、 ふとしたきっかけでonline-judge-tools の存在を知り、また自分の求めているものにかなりマッチしていると気づきました。

  • サンプルのダウンロード: oj download -d {{target_directory}} {{problem_url}}
    • オプションでダウンロード先を簡単に変更できるのが嬉しい。
  • サンプルのテスト: oj test -c "go run {{target_program}}" -d {{sample_directory}} -t 4 (Goの場合)
    • オプションで実行コマンドを変えられるので、他言語の対応も簡単そう。
  • コードの提出: oj submit -y {{problem_url}} {{target_source_file}}
    • 提出言語を推定してくれるので、ヒューマンエラーがない。

また、AtCoderのみならず、Codeforces、yukicoder、AOJといった主要なサイトに対応しているのも、非常にありがたいですね。

Vimから呼び出せるよう、連携しようという試み

私は普段エディタにVim(厳密にはneovimですが)を使っており、またVimの利点として 「ターミナルやCUIとの距離感が近い」 というものがあると思っています。 個人的には、他のエディタに比べて、CUIツールとの連携が簡単にできるのではないかと感じています。

よって、 「先述のojコマンドをいい感じに呼び出すVimのコマンドを定義すること」 を目指します。

仕様

Vimコマンドラインモード(コロン打ったら遷移するモード)から、以下のコマンドを打てるようにします。

  • サンプルのダウンロードコマンド: :DonwloadSamples
    • コマンドを実行すると、 今エディタに載っている問題のサンプルが同じディレクトリ階層にDLされる。
      • 例えばコンテスト中のコードは contests/2020/08/20200815_ABC175/a/a.go みたいに整理しているのですが、この問題のサンプルは contests/2020/08/20200815_ABC175/a/test/ ディレクトリに収まってほしい、という具合です。
  • サンプルのテストコマンド: :TestCurrentBuffer
    • コマンドを実行すると、 今エディタに載っているコードに対してすべてのサンプルが検証される。
      • 先程DLしたものが素直に実行されてほしい、という具合です。
  • コードの提出コマンド: :SubmitCode
    • コマンドを実行すると、 今エディタに載っているコードが対応する問題に対して提出される。
      • 検証が済んだらそのままの流れでシームレスに提出まで持っていきたい、という具合です。

コマンドのデモ

各コマンドの動作イメージは、以下のようなものになります。

f:id:maguroguma:20200819004910g:plain
サンプルのダウンロード

f:id:maguroguma:20200819004818g:plain
サンプルのテスト

f:id:maguroguma:20200819005716g:plain
コードの提出

各コマンドを定義するVim script

各コマンドについて1つずつ観ていきます。

" ファイル上部に記述される「問題のURL」を取得する関数
function! s:ReadProblemURLFromCurrentBuffer()
  let l:lines = getline(0, line("$"))
  for l:line in l:lines
    let l:record = split(l:line, ' ')
    for l:r in l:record
      let l:url = matchstr(r, '^\(http\|https\):.*$')
      if l:url != ''
        return l:url
      endif
    endfor
  endfor
  return ''
endfunction

はい、いきなり 「コンテスタントの運用でカバー」 的な要素があります。 この関数は、 現在ロードしているソースファイルの上部に「問題のURL」が記載されていることを期待 しています。

最初は、コマンドの引数に問題のURLを渡す設計で考えていたのですが、 このURLはコード提出時にも必要になることから、 「ファイル中のコメントとしてはじめに一度だけペーストしてしまうほうが、以降の手間もミスもなくなって良いのではないか?」 と思い、このようにしました。

なので、私の競プロのルーティンとして 「問題を開いたらURLをファイルのトップにコピーする」 というものが組み込まれることとなりました*4

そして、サンプルのダウンロードコマンドが以下になります。

" サンプルダウンロードのための関数とコマンド
function! s:MakeSampleDLCommand(url)
  let l:cur_buf_dir = expand("%:h")
  let l:target_dir = l:cur_buf_dir . "/test"
  let l:dl_command = printf("oj download -d %s %s", l:target_dir, a:url)
  return l:dl_command
endfunction
function! s:DownloadSamples(url)
  let l:command = s:MakeSampleDLCommand(a:url)
  echo "[Run] " . l:command . "\n"
  call execute('vs')
  call execute('terminal ' . l:command)
endfunction

command! -nargs=0 DownloadSamples :call s:DownloadSamples(s:ReadProblemURLFromCurrentBuffer())

やっていることは、 Vim scriptで本来ターミナルで実行したいコマンドを組み立てて、Vimtarminal コマンドに渡して実行させている」 、というだけです。 以降のコマンドでもそうですが、 system() 関数で実行し結果を echo するよりも、 見栄え的にこちらのほうがいい感じです(多分)。

続いて、ダウンロードしたサンプルの実行コマンドです。

" サンプルテストのための関数とコマンド
function! s:MakeTestSamplesCommand()
  let l:cur_buf_go = expand("%")
  let l:cur_buf_dir = expand("%:h")
  let l:sample_file_dir = l:cur_buf_dir . "/test"
  let l:test_command = printf("oj test -c \"go run %s\" -d %s -t 4", l:cur_buf_go, l:sample_file_dir)
  return l:test_command
endfunction
function! s:TestSamples()
  let l:command = s:MakeTestSamplesCommand()
  echo "[Run] " . l:command . "\n"
  call execute('vs')
  call execute('terminal ' . l:command)
endfunction

" Go版テスト実行コマンド
command! -nargs=0 TestCurrentBufferGoCode :call s:TestSamples()

これも、コマンドの組み立て部分が微妙に変わっただけで、ダウンロードとほとんど変わらないですね。 実行コマンドを差し替えたものを用意すれば、他の好きな言語の実行コマンドも作れると思います*5

最後に、コードの提出コマンドになります。

" コード提出のための関数とコマンド定義
function! s:MakeSubmitCommand(url)
  let l:cur_buf_go = expand("%")
  let l:submit_command = printf("oj submit -y %s %s", a:url, l:cur_buf_go)
  return l:submit_command
endfunction
function! s:SubmitCode(url)
  let l:command = s:MakeSubmitCommand(a:url)
  echo "[Run] " . l:command . "\n"
  call execute('vs')
  call execute('terminal ' . l:command)
endfunction

command! -nargs=0 SubmitCode :call s:SubmitCode(s:ReadProblemURLFromCurrentBuffer())

サンプルのダウンロードの際に必要となったURLが、ここでも必要となります。

以上で紹介したコマンドや関数は、すべてojが実行可能であることを前提としたものですので、 スクリプトファイルとするにあたっては、以下のように実行可能時のみ定義するようにするのが良いかと思います。

if executable('oj')
  " ファイル上部に記述される「問題のURL」を取得する関数
  function! s:ReadProblemURLFromCurrentBuffer()
  ...
  command! -nargs=0 SubmitCode :call s:SubmitCode(s:ReadProblemURLFromCurrentBuffer())
endif

最後に

もはやAtCoder Problemsと同じくらい「これがなきゃ競プロやってらんねぇ」なツールになってきたので、 感謝するだけじゃなくcommitできるようコード読まないとなぁと思います。

kmykさんおよびコミッタの皆様、本当にありがとうございます。

あと、これくらい簡単なVim scriptが書けるだけでも、自分用の便利コマンドは案外簡単に作れたりするので、ぜひVimを使いましょう!

*1:設定で、Vim内のコピー先をクリップボードと共有することはできますが、個人的に好きじゃないのでその点はデフォルトのままとしています。そのせいで以前に一度、直前の問題のコードを間違って貼って提出し、REしてペナを貰うというのをやったことがあります

*2:提示されているデフォルトの使用方法がマッチしているという方には、とても便利なツールなのだと思います。

*3:大方の機能は実装できたのですが、古いARCの問題のクローリングで早々とコケてしまい、途方に暮れていたところojに出会いました(圧倒的感謝)。

*4:案外気にならない上に、何度も提出する必要がある難しい問題になるほど、恩恵は大きくなります。後は、問題の復習をするときにコードからすぐに問題ページを開けるのもよいです。

*5:たまにBashの練習に競プロを使ったりするので、Bashバージョンも持っていたりします。

Goで再帰関数による全方位木DPを(可能な限り)抽象化

次に出題されたら絶対に落としたくないという気持ちから、再帰関数による全方位木DPを抽象化したコードを書いてみました。

セグメント木の際と同様に、型 T を都度書き換えないと駄目なのがかっこ悪いですが。。 どなたかいい方法を御存知でしたら教えて下さい。

type T int

type ReRooting struct {
    n int
    G [][]int

    ti      T
    dp, res []T
    merge   func(l, r T) T
    addNode func(t T, idx int) T
}

func NewReRooting(
    n int, AG [][]int, ti T, merge func(l, r T) T, addNode func(t T, idx int) T,
) *ReRooting {
    s := new(ReRooting)
    s.n, s.G, s.ti, s.merge, s.addNode = n, AG, ti, merge, addNode
    s.dp, s.res = make([]T, n), make([]T, n)

    s.Solve()

    return s
}

func (s *ReRooting) Solve() {
    s.inOrder(0, -1)
    s.reroot(0, -1, s.ti)
}

func (s *ReRooting) Query(idx int) T {
    return s.res[idx]
}

func (s *ReRooting) inOrder(cid, pid int) T {
    res := s.ti

    for _, nid := range G[cid] {
        if nid == pid {
            continue
        }

        res = s.merge(res, s.inOrder(nid, cid))
    }
    res = s.addNode(res, cid)
    s.dp[cid] = res

    return s.dp[cid]
}

func (s *ReRooting) reroot(cid, pid int, parentValue T) {
    childValues := []T{}
    nexts := []int{}
    for _, nid := range G[cid] {
        if nid == pid {
            continue
        }
        childValues = append(childValues, s.dp[nid])
        nexts = append(nexts, nid)
    }

    // result of cid
    rootValue := s.ti
    for _, v := range childValues {
        rootValue = s.merge(rootValue, v)
    }
    rootValue = s.merge(rootValue, parentValue)
    rootValue = s.addNode(rootValue, cid)
    s.res[cid] = rootValue

    // for children
    accum := s.merge(s.ti, parentValue)
    length := len(childValues)
    if length == 0 {
        return
    }
    if length == 1 {
        s.reroot(nexts[0], cid, s.addNode(accum, cid))
        return
    }

    // cid has more than one child
    R, L := make([]T, length), make([]T, length)
    L[0] = s.merge(s.ti, childValues[0])
    for i := 1; i < length; i++ {
        L[i] = s.merge(L[i-1], childValues[i])
    }
    R[length-1] = s.merge(s.ti, childValues[length-1])
    for i := length - 2; i >= 0; i-- {
        R[i] = s.merge(R[i+1], childValues[i])
    }

    for i, nid := range nexts {
        if i == 0 {
            s.reroot(nid, cid, s.addNode(s.merge(accum, R[1]), cid))
        } else if i == length-1 {
            s.reroot(nid, cid, s.addNode(s.merge(accum, L[length-2]), cid))
        } else {
            s.reroot(nid, cid, s.addNode(s.merge(accum, s.merge(L[i-1], R[i+1])), cid))
        }
    }
}

参考

ei1333さんによる全方位木DPの解説記事

ABC160-Fのすぬけさんによる解説

お二人の解説によって、全方位木DPがすっきりと理解できました。 解説を読んだ(聴いた)後、まずは抽象化を考えずに、素直にDFSを2回やる手法で、アドホックなコードを書いてみました。

いきなり抽象化から入るのは大変かもしれないので、まずはこれらを参照するのが良いかと思います。

keymoonさんによる全方位木DPの抽象化解説記事

こちらの記事を受けてGoによる実装(写経)を書いてみることで、 今回の再帰関数による抽象化も書くことができました。 keymoonさん、ありがとうございます!

Goでセグメントツリーを(可能な限り)抽象化

セグメントツリーをもう少し取り回しが効くようにしたいなぁと思った*1ので、 他の方々のブログ等を参考にしながら書き直してみました*2

実装

詳細は後述しますが、tsutajさん・ei1333さん・beetさんのお三方の解説を大いに参考にさせていただきました。

変数名やメソッド名はそれぞれの記事から拝借しているため、若干キメラになっていて読みにくいかもしれません。

また、数学的な側面の理解が浅いので、モノイド・作用素モノイド周りの命名が特に気持ち悪いかもしれません*3

通常(遅延伝搬なし)

type T int // (T, f): Monoid

type SegmentTree struct {
  sz   int              // minimum power of 2
  data []T              // elements in T
  f    func(lv, rv T) T // T <> T -> T
  ti   T                // identity element of Monoid
}

func NewSegmentTree(
  n int, f func(lv, rv T) T, ti T,
) *SegmentTree {
  st := new(SegmentTree)
  st.ti = ti
  st.f = f

  st.sz = 1
  for st.sz < n {
    st.sz *= 2
  }

  st.data = make([]T, 2*st.sz-1)
  for i := 0; i < 2*st.sz-1; i++ {
    st.data[i] = st.ti
  }

  return st
}

func (st *SegmentTree) Set(k int, x T) {
  st.data[k+(st.sz-1)] = x
}

func (st *SegmentTree) Build() {
  for i := st.sz - 2; i >= 0; i-- {
    st.data[i] = st.f(st.data[2*i+1], st.data[2*i+2])
  }
}

func (st *SegmentTree) Update(k int, x T) {
  k += st.sz - 1
  st.data[k] = x

  for k > 0 {
    k = (k - 1) / 2
    st.data[k] = st.f(st.data[2*k+1], st.data[2*k+2])
  }
}

func (st *SegmentTree) Query(a, b int) T {
  return st.query(a, b, 0, 0, st.sz)
}

func (st *SegmentTree) query(a, b, k, l, r int) T {
  if r <= a || b <= l {
    return st.ti
  }

  if a <= l && r <= b {
    return st.data[k]
  }

  lv := st.query(a, b, 2*k+1, l, (l+r)/2)
  rv := st.query(a, b, 2*k+2, (l+r)/2, r)
  return st.f(lv, rv)
}

func (st *SegmentTree) Get(k int) T {
  return st.data[k+(st.sz-1)]
}

おそらくは、モノイドの定義( T, f, ti )を適切に書き換えることだけに注力すれば、うまく動くのではないかと思います。 AOJにある典型例(RMQ, RSQ)は検証済みです*4

例: yukicoder No.875 Range Mindex Query

問題のURL

yukicoderの解説にもある通り、 最小値に加えて、最小値が入っているインデックスをもたせた構造体を要素の型とすればOKです。

以下はコードの抜粋です(提出はこちら)。

type T struct {
    v   int
    idx int
}

func main() {
    n, q := ReadInt2()
    A := ReadIntSlice(n)

    f := func(lv, rv T) T {
        t := T{}
        if lv.v < rv.v {
            t.v = lv.v
            t.idx = lv.idx
        } else {
            t.v = rv.v
            t.idx = rv.idx
        }
        return t
    }
    ti := T{v: 1<<31 - 1, idx: -1}
    st := NewSegmentTree(n, f, ti)
    for i := 0; i < n; i++ {
        st.Set(i, T{v: A[i], idx: i})
    }
    st.Build()

    for i := 0; i < q; i++ {
        c, l, r := ReadInt3()
        if c == 1 {
            ol := st.Get(l - 1)
            or := st.Get(r - 1)
            ol.idx, or.idx = or.idx, ol.idx
            st.Update(l-1, or)
            st.Update(r-1, ol)
        } else {
            e := st.Query(l-1, r)
            fmt.Println(e.idx + 1)
        }
    }
}

遅延伝搬あり

// Assumption: T == E
type T int // (T, f): Monoid
type E int // (E, h): Operator Monoid

type LazySegmentTree struct {
  sz   int
  data []T
  lazy []E
  f    func(lv, rv T) T        // T <> T -> T
  g    func(to T, from E) T    // T <> E -> T (assignment operator)
  h    func(to, from E) E      // E <> E -> E (assignment operator)
  p    func(e E, length int) E // E <> N -> E
  ti   T
  ei   E
}

func NewLazySegmentTree(
  n int,
  f func(lv, rv T) T, g func(to T, from E) T,
  h func(to, from E) E, p func(e E, length int) E,
  ti T, ei E,
) *LazySegmentTree {
  lst := new(LazySegmentTree)
  lst.f, lst.g, lst.h, lst.p = f, g, h, p
  lst.ti, lst.ei = ti, ei

  lst.sz = 1
  for lst.sz < n {
    lst.sz *= 2
  }

  lst.data = make([]T, 2*lst.sz-1)
  lst.lazy = make([]E, 2*lst.sz-1)
  for i := 0; i < 2*lst.sz-1; i++ {
    lst.data[i] = lst.ti
    lst.lazy[i] = lst.ei
  }

  return lst
}

func (lst *LazySegmentTree) Set(k int, x T) {
  lst.data[k+(lst.sz-1)] = x
}

func (lst *LazySegmentTree) Build() {
  for i := lst.sz - 2; i >= 0; i-- {
    lst.data[i] = lst.f(lst.data[2*i+1], lst.data[2*i+2])
  }
}

func (lst *LazySegmentTree) propagate(k, length int) {
  if lst.lazy[k] != lst.ei {
    if k < lst.sz-1 {
      lst.lazy[2*k+1] = lst.h(lst.lazy[2*k+1], lst.lazy[k])
      lst.lazy[2*k+2] = lst.h(lst.lazy[2*k+2], lst.lazy[k])
    }
    lst.data[k] = lst.g(lst.data[k], lst.p(lst.lazy[k], length))
    lst.lazy[k] = lst.ei
  }
}

func (lst *LazySegmentTree) Update(a, b int, x E) T {
  return lst.update(a, b, x, 0, 0, lst.sz)
}

func (lst *LazySegmentTree) update(a, b int, x E, k, l, r int) T {
  lst.propagate(k, r-l)

  if r <= a || b <= l {
    return lst.data[k]
  }

  if a <= l && r <= b {
    lst.lazy[k] = lst.h(lst.lazy[k], x)
    lst.propagate(k, r-l)
    return lst.data[k]
  }

  lv := lst.update(a, b, x, 2*k+1, l, (l+r)/2)
  rv := lst.update(a, b, x, 2*k+2, (l+r)/2, r)
  lst.data[k] = lst.f(lv, rv)
  return lst.data[k]
}

func (lst *LazySegmentTree) Query(a, b int) T {
  return lst.query(a, b, 0, 0, lst.sz)
}

func (lst *LazySegmentTree) query(a, b, k, l, r int) T {
  lst.propagate(k, r-l)

  if r <= a || b <= l {
    return lst.ti
  }

  if a <= l && r <= b {
    return lst.data[k]
  }

  lv := lst.query(a, b, 2*k+1, l, (l+r)/2)
  rv := lst.query(a, b, 2*k+2, (l+r)/2, r)
  return lst.f(lv, rv)
}

func (lst *LazySegmentTree) Get(k int) T {
  return lst.Query(k, k+1)
}

大抵はそうだろうということで、混乱しないように Assumption: T == E みたいなことを書いてしまいました。 しかし、次の例に示すように、別にそうとも限らないですね。。

こちらもモノイド、作用素モノイド、 そしてそれらに関する関数オブジェクトを適切に決定することに注力しさえすればいいように書いたつもりです。

例: yukicoder No.876 Range Compress Query

問題のURL

yukicoderの解説での想定解法は「階差数列に着目して区間加算を2箇所の点加算で済むようにする」 というようなもの*5のようです。 しかし、区間加算を真に受けて遅延伝搬セグメントツリーでも解けます。

与えられた定義で計算される圧縮値の他、区間の両端の値をもたせた構造体を要素の型とすればOKです。 単位元は少し注意が必要です。

以下はコードの抜粋です(提出はこちら)。

const INF_BIT60 = 1 << 60

func main() {
    n, q := ReadInt2()
    A := ReadIntSlice(n)

    f := func(lv, rv T) T {
        t := T{}
        t.v = lv.v + rv.v
        if lv.r >= INF_BIT60 || rv.l >= INF_BIT60 {
        } else if lv.r != rv.l {
            t.v++
        }
        t.l, t.r = lv.l, rv.r
        return t
    }
    g := func(to T, from E) T {
        to.l += int(from)
        to.r += int(from)
        return to
    }
    h := func(to, from E) E {
        return to + from
    }
    p := func(e E, length int) E {
        return e
    }
    ti := T{v: 0, l: INF_BIT60, r: INF_BIT60}
    ei := 0
    lst := NewLazySegmentTree(n, f, g, h, p, ti, E(ei))

    for i := 0; i < n; i++ {
        lst.Set(i, T{v: 0, l: A[i], r: A[i]})
    }
    lst.Build()

    for i := 0; i < q; i++ {
        c := ReadInt()
        if c == 1 {
            l, r, x := ReadInt3()
            lst.Update(l-1, r, E(x))
        } else {
            l, r := ReadInt2()
            t := lst.Query(l-1, r)
            fmt.Println(t.v + 1)
        }
    }
}

type T struct {
    v, l, r int
}

type E int

参考

学習の高速道路が整備されていてありがたいなぁと感じました。

tsutajさんの解説

tsutajさんの解説記事(遅延伝搬なし)

tsutajさんの解説記事(遅延伝搬あり)

いずれもとてもわかりやすく、初学時にセグメントツリーの仕組みを理解するのにはかどりました*6

セグメント木の抽象化だけに絞るのであれば、tsutajさんのブログを理解すれば十分な気がします。

ei1333さん・beetさんの解説

ei1333さんの旧ブログ記事での解説

beetさんの旧記事での解説

遅延伝搬セグメントツリーの抽象化は独力ではできる気がしなかったため、非常に参考になりました。 お二方とも旧ブログの方を参照してしまって恐縮です。 特にbeetさんの記事の方は本当に丁寧に説明されていてわかりやすかったです。


少数の定義を渡すだけで正しく動いてくれて感動したので、個人的には満足しています。

まだ解いた問題が少なすぎるので「これでOK」とは言い難いですが、 とりあえずは運用して様子見してみようと思います。

*1:解説で「セグメントツリーでも解けます」とあるときにテンション下がるのは嫌ですよね。

*2:言語を変えただけで、基本的に写経です。

*3:アドリブでいじるときに、自分にとって意味を思い出しやすいように意識して書いたらこうなりました

*4:Tの型が複雑になるときのインスタンス化の速度への影響が、まだ十分に検証しきれていません。

*5:そちらの解法で解き直していないので間違っているかもしれません。

*6:蟻本だけでは不安だったところを補完するのに活用させていただきました