Rust を使って気合を入れた開発を始めてみて中断もはさんで実質2週間ぐらいたちました。シンタックスにつっかえることも少なくなってきて最初の混乱期は乗り越えたかなと思います。忘れないうちに第一印象を書き残しとこうと思います。一言語との初対面は一生に一度かぎりだからね。
全体的にいいなと思うところが多いです。やっぱり一番気に入ったのはメモリの扱いですけれども、これはきっと記事も長くなるし自分の頭の中を整理するのにも時間がかかりそうなので今回は置いておいて、もうちっと表面的なところで気に入ったところから。
いい感じのコーディングスタイルで統一されている
厳密にはスタイルの方言は作ろうと思えば作れるらしい、と調べたらわかりましたけど、Rust プロジェクトが推奨しているスタイルがあってそれにのっとったスタイルチェッカが cargo に組み込まれているからもうこれはほぼ標準といっていいのでしょう。コーディングスタイルには長年苦労してきましたけど、わかってきたのが、方言は容認しない、コードレビューのたびに口うるさく言うのではなくチェックイン時にスタイルチェックをかけて守らないコードは受け付けない、もっと良いのは有無を言わさず自動フォーマット、という運用をするのが効果的だということ。Python は完全にこの流れで、pep8 という「標準」があるし、方言を全く受け付けない black というフォーマッタが多分一番人気です。C/C++ は clang-format が大勢を占めつつあるように見えるけれども方言がまだたくさんあります。Java はぐちゃぐちゃで何が何やらわかりません。golang は標準のスタイルを守らないとコンパイラが通りません。これはうざいもののまあ良いのですが、その標準スタイルが、改行を許さない実に読みにくいもので… まああまり興味がない言語でよく知らないのでこれぐらいで。こういった流れは各言語時間をかけてそうなっていったわけですが、Rust はそれを見越したのでしょうけど「オフィシャル推奨」のスタイルがあるわけです。そしてこれがかなり読みやすい。コーディングの準備にスタイルを選んだりスタイルチェッカを探したりの手間がないし後日変える必要もないし、いいと思いました。
ユニットテストが備え付け
ユニットテストは開発をしているとどうしても必要になってきますけれども、ユニットテスト用のフレームワークを探してくるのもまた面倒、ということで、ユニットテストもやっぱり統一されている方が助かります。Java は JUnit でほぼ決まりですけれども v4 と v5 で微妙に違っていて毎度バージョンを確認しなくてはならなくて面倒です。python は言語標準のものがいまいち使いにくいので pytest のほうが人気がありそうです。近頃は大抵どちらかなように感じています。標準のテストは pytest でも動くのでどちらかならまあ pytest だと思っていればよしです。JavaScript は Jest でしょうけれどもなんか似ているけど微妙に違う Jasmine というのもあったり Jest 自体も振る舞いが環境によって結構違ったりと、テストをちゃんと動かすのは毎度苦労しています。C/C++ は、もう何が何だかわからない。個人的には gtest を使ってます。こういうドタバタと比べると、Rust では言語自体にテストの仕組みはほぼ組み込まれていて、環境によってテストが動いたり動かなかったりとか、どのフレームワークを選べば?という悩みもないし、これまたいいなと思いました。
整数型の扱いにあいまいさがない
記憶領域を持っている計算機上で動くプログラムを書いている以上、整数型のバイト数がいくつか、符号付きなのか、符号なしなのか、ということを意識しながらコーディングをするのは非常に重要で、整数コピーをやり間違えると見つけるのが難しいバグのもとになりかねません。Rust では整数型の扱いがうるさいぐらい厳密で、コンパイラを通すのは手間ですけれどもコンパイラを通した後の安心感が違います。あと、char, short, int, long の名前ではなくて i8, i16, i32, i64 といった統一感のある型名もいいです。C/C++ でもほぼ int*_t 型しか使わないようにしていますが、言語標準ではないのがちょっと残念。Java はいちおう厳密さはありますがまだ中途半端、それに符号なし整数のサポートがないのが痛いです。符号なし演算はやっぱりどうしても必要な場面があって、本当の値を直接確認できないので目隠し的なコーディングが必要になってしまいます。C/C++ は整数の扱いがいい加減で本当に気を付けないと整数の扱い間違いでちょいちょいバグります。言語仕様上あいまいな整数コピーは許されているのですが、コンパイラの設定によって誤った整数コピーには警告を出すことができ、警告をエラー扱いにするコンパイラの設定もあり、これを入れておけばまだ良いのですが、忘れがちだしプロジェクトの途中でこの設定を入れるとエラーが多すぎて手に負えなくなったりもします。(昔警告4000か所越えで直すのをあきらめたことがある)
例外がない
今やっているようなシステム系のプログラミングではいかにエラーを整然と扱うかが全体の構成を崩壊させない鍵になりますが、何といっても難しいのが例外を投げた時にどの例外をどのレベルで受け止めてエラー後どう処理の流れを作るかを設計すること。設計しても正しく実装するには決めた規約をきちんと守らなくてはいけません。これは一人でコーディングしていても大変で、チーム開発になったら徹底するのはかなり難しいです。Rust では、言語は例外の機能を持たず、代わりに Result という enum を返すという「規約」をもとにエラー処理が組み立てられています。まだ慣れなくてこの周りのコードをきれいに書くのには苦労していますが、良いと思うことは
- エラーは一個上のレベルまでしか上がらない(関数の戻り値なので)
- 戻せるエラーは一種類しかない(エラーの設計が超面倒だけど決まったらエラーハンドリングは一種類で済む)
- ここはエラーは戻らないはず。戻ったらバグ、という場合には unwrap() を使う。バグったらパニックが起きるからすぐデバッグ
他の言語とあまりに違うので正直まだ慣れてはいないし、厳密すぎてコードが長くなって読みにくい、という場面も多々ありますけれども、エラー設計ミスでプロジェクト崩壊よりはましと思ってます。ここは少し時間をかけてコーディングの腕を上げて行くところかもしれません。ところで、まだ Rust でチーム開発はしたことないわけですが、このエラーは一種類で一レベル上までしか上がらない、という縛りはエラー処理をチーム内で徹底させる点においてかなり優れているのではないでしょうか?やってみないとわからないですけれども。
(いいか、な?)ポインタの代わりに Option を使う
Java でもほぼ同じような Option 型があって、しばらく使ってみてあまりに面倒で使うのをやめてしまったんですけれども、Rust ではほぼ強制といってよい作りです。ポインタを使ってオブジェクトを持ったり持たなかったりする場合 Option を使うほか良い方法がありません(知りません)。まあここでも、絶対に None (null) が来ないとわかっている場合には unwrap() を使って Option の中身チェックの煩雑なコードを避けることもできるので、こんなに厳密にやる意味あるのかな?と思わないでもないですが、unwrap() を使ったらそこは危険個所、というのは一目瞭然なわけで。慣れたら良い、のかも?まあすばらしく気に入っているわけではないですけれどもちょっとは安全なのかな?とは思います。
気に入らないことも少しは
大筋とってもいいなと思っている Rust 言語ですけれども、いやだなと思うところも少しはあります。慣れたら気にならなくなってくるかもしれませんけど。シンタックスに自由度がありすぎるのはいただけないと思います。一番しょっちゅう混乱するのが return は省略できること。以下の二つの関数が同じとか必要あったのか?と思います。
fn return_five() -> u32 {
5
}
fn return_5() -> u32 {
return 5;
}
Rustこんなコードにも要注意です
let value = match some_function() {
Ok(ret) => {
return "ok".to_string();
}
Err(e) => {
return format!("{:?}", e);
}
};
// そして処理は続く
Rustぱっと見ここで関数から戻ると思っちゃうじゃないですか。まあ慣れてきたけど。クロージャだと思えばいいんですよね。ですが、クロージャも書き方の柔軟性が高すぎと思います。引数が0個なら省略していいとか(あれ?いいんだよね?)。あと、match の書式とクロージャのとで一貫性がないのもどうかなと思います。
全体的に、Rust のコードのシンタックスは必要以上にわかりにくい気がします。この辺、呪文が多すぎて解読できないコードがやたらあって嫌になって使うのをやめた perl にちょっと似ている気がします。