Rust 標準 linter: Clippy
プログラミング言語には、よくある間違いや非推奨の書き方をチェックして警告を発してくれる、 lint というプログラムがあります。
元々は C 言語をチェックするものでしたが、現在では様々な言語のための linter が作られています。
例えば、 Python コードをチェックする pylint、ECMAScript をチェックする eslint、 C や C++ をチェックする clang-tidy というツールがあります。
Rust には言語標準の linter があり、その名を clippy と言います。
使い方は極めて簡単で、 cargo ツールチェインがインストールされていれば、下記のようにインストールして、
$ rustup component add clippy
下記のコマンドを crate のフォルダで実行するだけです。
$ cargo clippy
Clippy の linter としての特徴
linter はコードの品質を向上するために、多くの現場で使われているツールですが、実際には厳しすぎるルールや、実際の問題にそぐわないものも多くあります。
偽陽性(false positive)の検出です。
通常は設定ファイルや特殊なコメントをコードに埋め込むことによって、特定の lint の有効・無効を切り替えることになります。
これは無視できない労力で、 linter のバージョンを更新するたびに新たなルールに対応する必要が出てきたり、コメントによってコードが汚くなったりするデメリットもあります。
Clippy の特徴は、デフォルトの設定でもそのような擬陽性の警告が少なく、実際にコードの品質が向上したり、プログラマとしての知識が得られるのを実感できるような警告が多いということです。
例えば、次のような関数を見てください:
fn sum_squares(values: &Vec<i32>) -> i32 {
values.iter().fold(0, |acc, value| acc + value * value)
}
この関数は問題なく動きますが、 Idiomatic Rust ではありません。
Clippy は、次のような警告を出してくれます。
warning: writing `&Vec<_>` instead of `&[_]` involves one more reference and cannot be used with non-Vec-based slices
--> src/lib.rs:1:28
|
1 | fn sum_squares(values: &Vec<i32>) -> i32 {
| ^^^^^^^^^ help: change this to: `&[i32]`
|
= note: `#[warn(clippy::ptr_arg)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#ptr_arg
これは、 &Vec!
よりも &[]
のほうが汎用性が高いということで、より多くの場所で使えるということです。
Vec<T>
は &[T]
に暗黙に変換されるので、わざわざ Vec<T>
で宣言するということは、使える範囲を狭めるだけで何のメリットもないということです。
この関数は、機能性を全く損なわずに、次のように書き直すことができます。
fn sum_squares(values: &[i32]) -> i32 {
values.iter().fold(0, |acc, value| acc + value * value)
}
さらに、メッセージが指摘しているように、&Vec
はベクター型が持っているリファレンスへのリファレンスであり、
2重にリファレンスを取っているという点で冗長です。
ミュータブル参照 の場合
ところで、引数の型がミュータブル参照であった場合は話が別です。&mut Vec<T>
と &mut [T]
ではできることが異なります。
次のように、引数のベクター型のサイズを変えるような関数は、ミュータブルスライスで置き換えることはできません。
fn append_square(values: &mut Vec<i32>) {
values.push(values.iter().fold(0, |acc, value| acc + value * value));
}
この関数は次のように、ベクターの最後に新しい要素を追加します。
let mut vv = vec![1,2,3];
append_square(&mut vv);
assert_eq!(vec![1,2,3,14], vv);
置き換えようとすると、コンパイルエラーになります。
fn append_square_s(values: &mut [i32]) {
values.push(values.iter().fold(0, |acc, value| acc + value * value));
}
このため、 clippy はミュータブル参照に対しては警告を発しません。
まとめ
このように、 clippy はより良いプログラムを書くことができるように導いてくれる先生のような存在であります。もちろん、 clippy も完璧ではなく、偽陽性の検出もありますが、時として間違うのは人間の先生でも同じことです。
本来は、 linter とはそういう物だと思います。
他の linter の例
蛇足ですが、他の linter で苛立つケースも挙げておきましょう。
pylint には misplaced-comparison-constant というルールがあり、次のようなコードで警告を出します。
if 0 < ranking:
...
理由としては「定数を比較演算子の右に置いた方が意図が明確になり読みやすくなる」ということが挙げられており、次のように書くことを推奨されます。
if ranking > 0:
...
しかし、私はできる限り >
よりも <
を使うようにしています。その理由は複数の変数が関わってきたときに数直線上の並びにしたほうが視覚的に多次元空間をイメージしやすいからです。
if 0 < a and 0 < b and a + b < c:
...
特に数学では複数の比較演算子を一つの式に入れるときは a < b < c
と書くべしと教えられます。 b > a < c
では b
と c
の大小関係が曖昧になるからです。ときに Python は複数の比較演算子を書ける珍しい言語なのですから、数式上と同じように書けると良いですよね。
if 0 < x < c:
...
とは言え、私は >
を使うスタイルを否定しているわけではありません。それを使った方が読みやすくなるケースもあるでしょうし、コーディング規約があればそれに従います。
問題はこのスタイルに一般的なコンセンサスが存在しているわけではないということであり、どちらが「より正しい」と言えるようなものではないという点です。linter が一方のスタイルを推奨するのをデフォルトにしているのはフェアとは言えません。
また、 pylint のドキュメントの簡素さを clippy のそれと比較しても、理由付けがあまりにも単純すぎ、サンプルコードもなく、どれだけ検討したのかと疑いたくなります。
もちろん設定でこの lint に関してオフにすることもできますが、設定ファイルやコード中のコメントで除外することになり、コードノイズが増えます。これはほんの一例にすぎませんが、多くの linter はこのような「好み」の問題と実際にプログラムの安全性に関わる問題やよくある落とし穴の検出が混ざっており、ノイズをフィルタする負荷はプログラマにかかることになります。
linter とは、手間を増やすのではなく確実にコードの品質を向上させるようなところに限って機能するのがあるべき姿だと思うのですが、どうでしょうね。