RAS Syndrome

冗長。

スニペット管理コマンドツールの構想

きっかけ

競プロでよく出てくるコード片を、私は 1 つのリポジトリにまとめている。
テストコードも書いたりして管理している。
コンテスト参加中はこのリポジトリを開いて、欲しいコードを探してコピペしてくる運用だ。
まあしかしこの運用は、競プロというタイムアタック競技においては致命的と言っていいほど、手間と時間がかかってしまう。

というわけで、コード片(=スニペット)を手軽に挿入するためのやり方を模索する。

"スニペット"機能を使うか否か

こういった時、他の人はどうしているか。
エディタの"スニペット"機能を活用しているパターンを多く聞く気がする。
キーワードを打ち込んで tab を押すだけで、設定しておいたコード片を挿入できるやつ。
プレースホルダーを設定された部分は書き換えることもできたりする。

私のリポジトリからコード片を吸い出し、上手いことエディタ(例えば VSCode)の"スニペット"設定に変換し、同期する。
そんなツールを作ることは簡単だろう。
というか、既存のツールでありそうな気もする。

ただし、設定はあくまでエディタ依存だ。
例えば複数エディタを使い分けたりするなら、どのエディタにも設定を同期しておかなければいけなかったりする。
私は個人的にそこが気に入らない。

あるいは、エディタとは独立したスニペット管理アプリを使うことも考える。
なんかそういうのがあるのは聞いたことがある。
でもなんか、そんな色んなツールに依存するのもだるい気がする。

もっとこう、コマンドラインから一発で挿入できればそれで良い気がする。
私が管理しようとしているコード片は基本的に Go で書かれているのだが、Go のエコシステムでは「ジェネレーターを使う(作る)」というパターンが割と一般的だ。

cf.
Generating code - The Go Blog
GoGenerateTools · golang/go Wiki · GitHub

メタプログラミングの発想をすることはちょくちょくあっても、メタプログラミング部分を実際のコードとは切り離した形で運用する、という発想は個人的には見落としがちなので、おもしろい。
Go の言語仕様は素朴で、Ruby みたいにメタプログラミング向けの言語機能が充実しているわけではない。だからこその文化なのだろうか。(まあしかし Ruby でも rails generate とかあったりするので、これまた興味深い。)
まあとにかく、コード片はコマンドラインで管理してみたい。

go generate を上手く活用する手もあるのかもしれない(よく分かってない)が、とりあえずは普通に標準出力にコード片を吐き出すだけのコマンドがあれば十分な気がする。
仮にコマンド(≒ツール名)を gnip としよう。(go と snippet から想起した造語。clip とも似ている。グニッって感じで音的にも気持ちいい。)
gnip sieveOfEratosthenes | pbcopy という感じでクリップボードに出力してしまえば、あとはペーストするだけでどこにでも挿入が可能だ。

案1. 探索パス内からコード片を探し出す

実装案の話。

最初に思いついたのは、「コマンド上で探索パスを登録できるようにする」という仕様だ。
前述の競プロ用リポジトリgnip --register ~/Projects/src/github.com/ikngtty/go-contestlib/ という感じで登録しておく。
実際にやることとしては、テキトーな設定ファイル(~/.gnip みたいな)に保存しておく感じでいいだろう。
そんでいざ gnip sieveOfEratosthenes という感じでコード片を呼び出そうとした時には、探索パス上のコードファイルからキーワード(この場合 sieveOfEratosthenes)に当てはまるコード片を見つけ出して出力する。
「キーワードに当てはまる」とは具体的には、「関数名と一致する」で良いかなと思っている。
あるいは、コメントで

// gnip: start sieveOfEratosthenes
func generatePrimes(max int) []int {
    return nil
}
// gnip: end sieveOfEratosthenes

みたいに指定するのも良さそうだ。
実装はこの方が楽だろう。

"スニペット"の管理ということで、やはりプレースホルダー機能も搭載したい。
Go は現在ジェネリクスが存在しないが、プレースホルダーを使えば任意の型についての型が量産できる。
例えば List<T> が作れない代わりに、{1:Int}List というスニペットから IntListStringList がすぐに作れる。
interface{} のリストを作るよりは、個人的にはこの方が良さそうに思う。
interface{} のリストから値を取り出したら、わざわざ変換しないといけない。これは書く際にもコストだし、実行時間としてもコストになりそうな気がする。競プロにおいてはどちらも避けたい。
そんなわけでプレースホルダー機能は欲しい。

しかし、これは結構めんどくさいことに気づく。

// gnip: start list
type anyList struct {
    headItem anyListItem
    tailItem anyListItem
}

type anyListItem struct {
    parent anyListItem
    value  interface{}
}
// gnip: end list

は go のコードだ。実際に動かせるし、テストもできる。だが、

// gnip: start list
type @{1:int}List struct {
    headItem @{1:int}ListItem
    tailItem @{1:int}ListItem
}

type @{1:int}ListItem struct {
    parent @{1:int}ListItem
    value  @{1:int}
}
// gnip: end list

みたいに独自の文法でプレースホルダーを導入しようとすると、これはもう go のコードではない。
強いて言えば .go.template のような拡張子になるだろう。
gnip --template list.go をすると list.go.template が自動で生成される。後は手動でプレースホルダーをメンテする。探索対象は拡張子が .template のファイルのみ。そんな感じ。
元コードに変更があった時の同期が大変そうだ。どうしよう。
一応

// gnip: start list any
type anyList struct {
    headItem anyListItem
    tailItem anyListItem
}

type anyListItem struct {
    parent anyListItem
    value  interface{}
}
// gnip: end list any

みたいにして、「gnip list int とすれば anyint に置換されますよ」みたいな仕様にもできる。
が、これは高確率で事故る。例えばコード辺の中に Many という単語が含まれていたら Mint になってしまったりする。
なのでさすがにこれはボツ。
そんなこんなでとにかく、プレースホルダー機能はめんどくさい。捨てた方がいいかもしれない。ジェネリクスもそのうち来るはずだし。

ところでこの .go.template ファイルであるが、どこに保存するの?という問題も残る。
よそのリポジトリにある .go ファイルの隣に勝手に .go.template ファイルを追加する。
追加された .go.template は、もちろん、そのリポジトリ上でコミットしておかないといけない。
これはちょっと嫌な感じ。
そもそも、// gnip: start とかいうコメントを入れるあたりから、そのリポジトリはもう gnip への依存が始まっている。
つまり、そのリポジトリは独立した意味をあまり持っておらず、gnip 配下のプロジェクトになっている。
こうなってくると、リポジトリを独立させて管理する意味があまりない気がする。
最初から gnip 専用のスペースでコード片を全部管理すればいいような気がする。

案2. コード片をツールに含める

コード片の管理場所。
素直に考えれば設定ファイル同様、.gnip/snippets/ みたいな専用フォルダを設けるのが第一案だ。
もちろん、フォルダパスは設定で変更可能にしても良い。
この場合、コード片のデバイス間同期をどうするかが気になるところだ。
とりあえずはユーザー(まあ自分以外のユーザーを想定する必要もあんまりないけど)が適当にハックすれば、.gnip/snippets/*GitHub リポジトリとかの何らかのクラウドに上げて同期するようにはできる。
余裕があればツール側でサポート機能を作っても良い。

一方で、どうせ GitHub 上でコード片を管理したいなら、いっそ gnip 本体のコードと同じリポジトリで同時に管理しちゃう運用がいいかな、という気もしている。
一応ベースプロジェクトとして、gnip 本体コードのみのリポジトリは作っておく。
ユーザーはそれをフォークして、自分の登録したいコード片を専用フォルダ下に追加してコミットし、自分専用の gnip プロジェクトを育てていく。
gnip 本体にアップデートがあった時は、upstream ブランチを良い感じでマージとかすれば問題ない。
私は設定の変更履歴とかもコミットコメントで記録しておきたいタチなので、この作戦は良い感じに work しそうだと感じている。
今のところ最有力の案だ。

と、まあ

ここまでアイディアメモ。
scrapbox で箇条書きにしとけって感じだが、なんとなくナラティブにまとまりそうだったので記事にした。(まあまとまってないんだけど。)

これから諸々の既存ツールの仕様とかを調べてみる。
スニペット管理コマンドツール、普通に既にありそうな気もする。
車輪の再発明が好きなので、あえてちゃんと調べずにここまで思索した。

おわり。