まえがき
お久しぶりです入茶してからものんびり競プロを進めているホベねこです。もうそれはそれはのんびり楽しくやっていたのですが、問題を解いているうちに似たような処理を書くことが増えてきて、自作ライブラリを作ろうとしたことが何度かありました。
自作ライブラリ管理したーい!!!
さて、Rust競プロerが自作ライブラリを管理しようとしたとき、候補に挙がってくる有名ツールとして、スニペット管理ツールであるcargo-snippetと、ソースコードバンドルツールであるcargo-equipがあると思います。僕も以前から気になっていたので、じゃあ入れようとしてみたのですがcargo-equipに関してはRust-Analyzerとのバージョン関係でうまくいかず入れることすらできず、Cargo-snippetは入れることはできたものの、関数単位でのsnippet登録のためwhile文を用いたBFSなどは登録することができなかったり、プレースホルダーも使えません。そもそもコードがかさばって見にくいなと感じ全く使いませんでした。cargo-equipも、なんか改造して使えるようにしてた人もいましたが、結局ライブラリを全部展開するのでコードが膨大になってしまいます。
ないなら...
cargo-equipはうまく入らない、cargo-snippetは少し使いにくい...さてどうしましょう...そう、
ないならつくればいいのです!!!
ちょうど情報の授業で何か作品を作らないといけなかったのでいい機会
使い方
不満たらたら垂れ流してたってせっかくやってきてくれた読者をブラウザバックさせるだけなのでとっとと今回僕が作ったcargo-snippet-moreの使い方に移ります。
Installation
rustup component add rustfmt
cargo install cargo-snippet-more --features="binaries"
Snippet機能
まずはcargo-snippetから引き継いだsnippet機能についてです。dependenciesにcargo-snippet-moreを追加しておきます。
[dependencies]
cargo-snippet-more = "0.1"
cargo-snippetとの変更点
use cargo_snippet_more::snippet;
#[snippet(name = "mymath", not_library)]
#[snippet("gcd")]
fn gcd(a: u64, b: u64) -> u64 {
if b == 0 {
a
} else {
gcd(b, a % b)
}
}
#[snippet(include = "gcd")]
fn gcd_list(list: &[u64]) -> u64 {
list.iter().fold(list[0], |a, &b| gcd(a, b))
}
#[snippet(name = "mymath", not_library)]
#[snippet]
fn lcm(a: u64, b: u64) -> u64 {
a / gcd(a, b) * b
}
#[test]
fn test_gcd() {
assert_eq!(gcd(57, 3), 3);
}
基本的にはcargo-snippetと同じです。not_libraryという見慣れないものがありますね。これはbundle機能の部分で詳しく解説しますが、ここでは別の名前が付けられた複数のItemをまとめたsnippetには付与する必要があると思っておけば大丈夫です。
範囲指定型snippet
use cargo_snippet_more::{snippet_end, snippet_start};
snippet_start!(name = "uf", library = "UnionFind");
struct UnionFind {
parent: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
UnionFind {
parent: (0..n).collect()
}
}
}
snippet_end!("uf");
snippet_start!(name = "extended_gcd", include = "gcd");
fn extended_gcd(a: i64, b: i64) -> (i64, i64, i64) {
// gcd関数を用いた処理
}
snippet_end!("extended_gcd");
fn bfs() {
let n = 0;
let g = vec![vec![]];
snippet_start!("bfs");
let mut dist = vec![!0; n];
let mut q = std::collections::VecDeque::new();
q.push_back(0);
dist[0] = 0;
while let Some(pos) = q.pop_front() {
for &i in &g[pos] {
if dist[i] == !0 {
dist[i] = dist[pos] + 1;
q.push_back(i);
}
}
}
snippet_end!("bfs");
}
cargo-snippet-more独自機能①、範囲指定型snippetです。同じ名前が付けられたsnippet_start, endマクロで囲まれた部分がsnippetとして抽出されます。
これにより、以前までstructやimplすべてに#[snippet]を付けないといけなかったUnionFindや複雑な構造体のsnippetを楽にわかりやすく追加することができるようになりました。
また、BFSなど一つのItemとして使うというより一連のStatementのまとまりとして使うことが多いアルゴリズムに対してもsnippetを設定できるようになりました。
もちろん、doc_hiddenやincludeなど既存の#[snippet]のパラメータも同じようにすることができます。
但し、一つだけ違う点として、libraryの指定の仕方があります。これも詳しくはbundle機能で解説しますが、簡単に言えば範囲指定型snippetの場合libraryはその構造体名で明示指定しないといけないということです。
プレースホルダー機能
cargo-snippet-more独自機能②、プレースホルダーです。プレースホルダーと言っているのはエディタなどでデフォルトで入っているif文などのsnippetを展開したときに条件式 -> 中身のように順番にカーソルが動いて入力できるあれです。
use cargo_snippet_more::p;
p!(0); // → $0
p!(1); // → ${1}
p!(1, variable); // → ${1:variable}
p!(1, |"option1", "option2", "option3"|); // → ${1|option1,option2,option3|}
少々わかりにくいかもしれませんがこれがそのプレースホルダーを指定するためのp!マクロです。コメントに書いてあるように、マクロを書くことで自動的に指定したsnippet形式のプレースホルダの形に変換され出力されます。
use cargo_snippet_more::{snippet, p};
#[snippet("binary_search")]
fn binary_search<T: Ord>(arr: &[T], target: &T) -> Option<usize> {
let p!(1, mut low) = 0;
let p!(2, mut high) = arr.len();
while low < high {
let p!(3, mid) = low + (high - low) / 2;
match arr[mid].cmp(target) {
std::cmp::Ordering::Less => low = mid + 1,
std::cmp::Ordering::Equal => return Some(mid),
std::cmp::Ordering::Greater => high = mid,
}
}
p!(0);
None
}
変数宣言や参照している変数の名前、matchなどマクロがおけるところならどこにでも使えます。
bundle機能
cargo-equipとの融合機能、bundle機能です。これは目的のファイルの中で使われているローカルコードを検出して作成したsnippetからそのコードを取得、その場でファイル内に展開する機能です。
まずCargo.toml内に追加する必要がある項目についてです。
[dependencies]
cargo-snippet-more = "0.1"
libs = { path = "../my-library" }
[[package.metadata.cargo-snippet-more.library-path]]
libs = "../my-library/libraries.toml"
-
dependencies内にはcargo-snippet-moreと自作ライブラリを追加しておきます -
package.metadata.cargo-snippet-more.library-pathにはcargo-snippet-more snippetを実行したときに同時に生成されるlibraries.tomlへのパスを書いておきます
dependencies内の自作ライブラリにつける名前と、package.metadata.cargo-snippet-more.library-path内の名前は一致させる必要があります
次に以下のコマンドを実行します
cargo-snippet-more init
このコマンドによりcargo-snippet-moreにCargo.tomlの内容を読んでbundleに必要な情報を追記させます。
ここではbundle対象の例として、以下のファイルを用います。
use libs::common::gcd;
use libs::graph::UnionFind;
fn main() {
let result = gcd(48, 18);
println!("GCD: {}", result);
let uf = UnionFind::new(10);
// ... ufを用いた処理
}
そうしたら次のコマンドを実行します。
cargo-snippet-more bundle --bin a
するとsrc/cargo-snippet-more/a.rsに
/* use libs::common::gcd; */
/* use libs::graph::UnionFind; */
fn main() {
let result = gcd(48, 18);
println!("GCD: {}", result);
let uf = UnionFind::new(10);
// ... ufを用いた処理
}
// The following code was expanded by `cargo-snippet-more`.
#[allow(unused_macros)]macro_rules! p{(0)=>{};($n:literal)=>{};($n:literal, |$first:tt $(,$rest:tt)*|)=>{$first};($n:literal, $($t:tt)*)=>{$($t)*};}pub fn gcd (a : usize , b : usize ) -> usize {if b == 0 {a } else {gcd (b , a % b ) } } pub struct UnionFind {par : Vec < usize > , siz : Vec < usize > , } impl UnionFind {pub fn new (n : usize ) -> Self {Self {par : (0 .. n ) . collect () , siz : vec ! [1 ; n ] , } } pub fn root (& mut self , x : usize ) -> usize {if self . par [x ] == x {return x ; } self . par [x ] = self . root (self . par [x ] ) ; self . par [x ] } pub fn unite (& mut self , mut parent : usize , mut child : usize ) -> usize {parent = self . root (parent ) ; child = self . root (child ) ; if parent == child {return parent ; } if self . siz [parent ] < self . siz [child ] {std :: mem :: swap (& mut parent , & mut child ) ; } self . par [child ] = parent ; self . siz [parent ] += self . siz [child ] ; parent } pub fn is_same (& mut self , u : usize , v : usize ) -> bool {self . root (u ) == self . root (v ) } pub fn size (& mut self , x : usize ) -> usize {let root = self . root (x ) ; self . siz [root ] } }
というファイルが作成されます!ちゃんと展開され、ローカルライブラリなしで実行できる状態になってますね!もし、cargo-competeでのtestやsubmissionをしたかったら
cargo compete test <contest_name>-<bin_name>-more
のようなコマンドを実行することで実現できます。
また、snippetを展開したときにcargo_snippet_more::expanded!("<name>");という何やら怪しげなマクロとともに展開されていることに気づいた方もいらっしゃるかもしれません。これは、cargo-snippet-moreに「もうこのsnippetは展開されているからbundle時に展開しなくていいよ!」ということを伝えるためのマクロです。このマクロをうまく利用すると、以下の二点のようなことができます。
i. 自作ライブラリAに依存してる自作ライブラリBを使うときに自作ライブラリAの内容を編集する
use libs::common::gcd_list;
cargo_snippet_more::expanded!("gcd");
fn gcd(a: usize, b: usize) -> usize {
println!("Computing GCD of {} and {}", a, b);
if b == 0 { a } else { gcd(b, a % b) }
}
fn main() {
let result = gcd_list(&[48, 18, 12]);
println!("{}", result);
}
このコードでは一見自作ライブラリのgcd_list関数が呼ばれるだけに見えますが、expanded!マクロによりgcd関数の展開がスキップされるので、gcd関数を参照しようとするgcd_list関数だけが展開されます(以下のコードは展開後にrustfmtしたもの)。
/* use libs::common::gcd_list; */
/* cargo_snippet_more::expanded!("gcd"); */
fn gcd(a: usize, b: usize) -> usize {
println!("Computing GCD of {} and {}", a, b);
if b == 0 {
a
} else {
gcd(b, a % b)
}
}
fn main() {
let result = gcd_list(&[48, 18, 12]);
println!("{}", result);
}
// The following code was expanded by `cargo-snippet-more`.
#[allow(unused_macros)]
macro_rules! p{(0)=>{};($n:literal)=>{};($n:literal, |$first:tt $(,$rest:tt)*|)=>{$first};($n:literal, $($t:tt)*)=>{$($t)*};}
pub fn gcd_list(list: &[usize]) -> usize {
list.iter().fold(list[0], |a, &b| gcd(a, b))
}
よって、実行時にはsnippetで展開したgcd関数が呼ばれることになり、Computing GCD of 48 and 18などの文字列が出力されます!
ii. 外部ライブラリを参照対象にする
似たようなことですが、自作ライブラリだけでなく外部ライブラリを参照対象にすることができます。
use superslice::*;
use my_library::get_lower_and_upper_bound;
cargo_snippet_more::expanded!("lower_bound");
cargo_snippet_more::expanded!("upper_bound");
fn main() {
let b = [1, 3];
let (l, w) = get_lower_and_upper_bound(b);
println!("lower_bound: {}, upper_bound: {}", l, w);
}
上記コードでは自作のlower, upper_bound関数を呼び出し変数l, wに格納しています。これも一見自作ライブラリのlower, upper_bound関数を呼び出してるだけですが、例のごとくexpanded!マクロによりupper, lower_bound関数の展開がスキップされているので、get_lower_and_upper_bound関数が参照するupper, lower_bound関数はsupersliceクレートのものになります。
not_library, libraryパラメータについて
一通りbundleについて説明できたところで、後回しにしていたnot_libraryやlibraryパラメータについての説明をします。
もともとのcargo-snippetの仕様だと、複数の関数や構造体、マクロに対して同じ名前のsnippetアトリビュートを付与することでそれらを一つのsnippetにまとめることができます。
また、cargo-snippet-moreではcargo-snippet-more snippet実行時にsnippet単位でsnippetの内容をまとめた後、そのsnippet内で最初に取得できた関数や構造体名を含めたpathでまとめてlibraries.tomlに保存し、bundle時にuseのpathとlibraries.tomlの内容を見比べて展開対象の関数などを特定します。
しかし、snippet内で最初に取得できた関数や構造体名を含めたpathで最終的に管理するので、その複数まとめたsnippetがそのpathの内容だと誤解され、いらぬものまで展開されるという悲劇が起こることがあります。これを防ぐために、複数の関数や構造体をまとめたsnippetに対してはnot_libraryを付与する必要があるというわけです。
一方範囲指定型snippetでは内部の仕組み上関数や構造体の名前を取得するのは困難です。よって、範囲指定型snippetを用いる場合、明示的にlibraryでその名前を指定する必要があります。これをもとにしてpathを特定するので、ここで指定する名前は任意のものではなくuseされるときに使われるその関数や構造体、マクロ名と一致させる必要があります。
あとがき
かなり複雑になってしまいましたが、これでやっと目的のものを作ることができました。最初はどうなることかと思いましたが...(無計画)
今回作りながらわかったことは、
- 想像よりもsynは各visitやリアルタイム編集など高度なことができる
- copilotの補完はかなり強い
- github educationは相当強い
- code agentは外から指示出すだけでやってくれるのでレビュー能力さえあればかなり時間を短縮できる
ということです(3/4関係ない)。
一応自分のatcoder環境やsnippet環境をここで共有しておきます。複数コマンドをshellscriptでまとめてたりするので環境構築で詰まった時や、snippetの書き方がわからなくなったときの参考にしていただければ幸いです!
ということで皆さん、こんなバージョンアップして高機能になったcargo-snippet-moreを使ってより快適な競プロライフを楽しんでみてはいかがでしょうか?ではまたっ!