はじめに
Rustは所有権など新しい概念が多く学習難易度が高いと言われています。
しかし個人的な感触としてはかなりソースコードの読みやすさに振った言語ではないかと感じているので、
そのあたりを説明したいと思います。
内容的に論理的な話というより感覚的な話が多く出てきますのでポエムということにしておきます。
ちなみに筆者がこれまで経験してきた言語はC/C++/C#/Java/Python/Ruby/Scalaあたりです。
そのためどうしても「それらの言語と比べてどうか」という話になりがちですが、
Rustが他言語より優れている/劣っているといった主張をするつもりはなく、
あくまで目指している方向が違う(と個人的に思っている)のだとご理解ください。
数値型
Rustの数値型は例えば
i32: 符号付き32bit整数
u128: 符号なし128bit整数
f64: 64bit浮動小数点数
isize: 符号付き整数(ポインタと同じサイズ)
といった感じです。intが何ビットか、といったことを覚えておかなくてもビット数が明確ですし、アーキテクチャ依存のある型(usizeとisize)が分離されているのも分かりやすいと思います。
また、Rustは数値型間の暗黙のキャストを許容しないので、(たとえi32 -> i64のように明らかに情報が落ちない場合でも)型変換は必ずソースコード上に明示されることになります。
これは記述が増えるので嫌われる傾向にあると思いますが、個人的にはどこで型が変わったかソースコードを追う必要がなくなるという点で、明示されている方が読みやすく感じます。
letとlet mut
letは不変の変数宣言、let mutは可変の変数宣言です。
基本的にはできるだけ不変で宣言したいので、可変の場合だけキーワードを追加するのは理にかなっていると思います。
let x = 0;
let mut y = 0;
y = 1;
例えば同じように可変と不変を宣言できるScalaの場合は以下のようになります。
こちらはこちらで縦方向に揃うのが好きなのですが、valとvarの区別がし辛いのでmutの方がより可変であることが分かりやすいと思います。
(またRustの方はあえて縦方向に揃えないことで、注目を促しているようにも見えます)
val x = 0;
var y = 0;
y = 1;
シャドーイング
Rustでは以下のように同一スコープで同名の変数を再度宣言することが可能です。
(これは代入ではないので、2つのvalueは型が違っていてもいいですし、mutも必要ありません)
let value = "0";
let value: i32 = value.parse().unwrap();
この挙動はあまり一般的ではないので初見殺しと言われたりするようですが、変数名をシンプルに保つ効果があるのではないかと思っています。
(他にもmutをつけたり外したりするのにも使うので、そのためだけの機能ではないですが)
例えば同じコードをC#(Parseの字面が似ているので選びましたが他の言語でも似たような感じになると思います)で書くと以下のように型毎に変数名を変える必要があります。
この_strや_intは本来変数名に必要な情報ではないのではないか、ということです。
string value_str = "0";
int value_int = Int32.Parse(value_str);
初めてRustのシャドーイングの仕様を見たときは「これは想定外の箇所でシャドーイング起こしてバグの原因になるのでは?」と思ったのですが、5年ほど書いて特に困ったことも起きていないです。
型検査がしっかりしているので、予想外のシャドーイングが起こった場合すぐコンパイルエラーとして検出されるからなのかな、と思っています。
エラー処理
Rustにはいわゆる(C++やJavaにおける)例外はなく、失敗する可能性のある処理はResultという型を返すことで表します。
fn failable() -> Result<i32, Error>;
これは特に関数呼び出し側の読みやすさに寄与していると感じます。
例えば上記のfailable関数の呼び出しは以下のようになります。
let x = failable()?; // 失敗したらearly return
let y = failable().expect("failed at y"); // 失敗したら"failed at y"を表示して強制終了
let z = failable().unwrap(); // 失敗したら強制終了
let w = failable();
...
w.unwrap(); // 後でエラー処理する場合
このように失敗する可能性のある関数は必ず失敗したらどうなるかを呼び出し時(あるいは結果を使う前)に明示することになります。
明示しなければコンパイルエラーとなるので、忘れるということもありません。
例外の場合、各関数からどのような例外が上がってくるのかをソースコード上から知る術はなく、
いつも「必要なcatchを網羅出来ているんだろうか?」という漠然とした不安があったのですが、
Rustの方式では(ちゃんと処理するか無視するかは別として)全てのエラーになんらかの対処をしている、と感じられるように思います。
まとめ
以上、個人的にRustが読みやすいと思っている部分についてご紹介しました。
Rustというと「所有権が~」という話になりがちですが、こういう特徴もあるよ、ということで。