4月16日、Borretti氏が「Two Years of Rust」と題した記事を公開し、注目を集めている。この記事では、Rustを用いた2年間の開発経験の振り返りとRust言語の特徴について詳しく紹介されている。
以下に、その内容を紹介する。
この記事では、大きく「良い点(The Good)」と「悪い点(The Bad)」に分けてRustの特性が語られている。
The Good
Rustの良い点として、筆者は以下を挙げている。
パフォーマンス(Performance)
Rustは高速に動作する。どの言語でも低速なコードを書くことは可能だが、Rustはボトルネックを取り除けばほとんどの場合十分に速く動くという。Pythonなどの言語では言語自体の性能限界で足を引っ張られがちだが、Rustは「言語としての性能天井」が高いため、最適化の余地が大きいことを強調している。
ツールチェーン(Tooling)
Rustのビルドシステム兼パッケージマネージャであるCargoは非常に優れており、エラーもほぼなく、挙動が予測可能で扱いやすいと評価している。依存関係の管理が自動的かつ宣言的で、他の言語にありがちな環境変数設定や仮想環境の煩雑さなどがない点が高く評価されている。
型安全性(Type Safety)
Rustは合併型(sum types)やオプション型、厳密な型変換ルールを備えており、それによって非常に堅牢なコードが書けると述べられている。コンパイラによって型安全性が保証されるため、テストを作ってもバグが検出されにくいほどに壊れにくいコードを書くことができるという。
エラーハンドリング(Error Handling)
Rustは例外を使わず、Result<T, E>
型と?
演算子を組み合わせることで、Goのような“エラーを戻り値として返す”スタイルとJava/Pythonのような“例外処理”のいいところを両立している。以下のようなコードが、
fn foo() -> Result<(), DbError> {
let db = open_database(path)?;
let tx = begin(db)?;
let data = query(tx, "...")?;
rollback(tx)?;
Ok(())
}
コンパイラの糖衣構文によって、次のような詳細な match
連鎖に相当する点が特徴だ。
fn foo() -> Result<(), DbError> {
let db = match open_database(path) {
Ok(db) => db,
Err(e) => {
return Err(e);
}
};
// ...(中略)...
Ok(())
}
借用チェッカー(The Borrow Checker)
Rustの最大の特徴である借用チェッカーによってメモリ安全性を確保している。筆者自身は別のプログラミング言語の研究を通じて借用チェッカーを深く理解していたため苦労は少なかったが、一般には学習段階で「コンパイラと戦う」ような体験があるとも言われる。コツとして、C/C++のようなコードをそのままRustに置き換えるのではなく、Rustの線形型やライフタイムの考え方に沿った設計を行うことが重要だとしている。
非同期処理(Async)
「色付き関数問題(coloured functions)」(※)としてよく批判される非同期処理だが、筆者は「OSスレッドのパフォーマンス制約を考えれば、スタックレスコルーチン(Rustのasyncモデル)は高い性能を得られる現実的な解だ」としている。ライブラリ実装者や言語仕様策定者には複雑であっても、利用者としてはasync fn
とawait
を付けるだけで使えるため、それほど大きな問題とは感じないという。
※「色付き関数問題」とは、非同期処理を導入した言語やライブラリでしばしば発生する設計上のジレンマを指す。同期関数(通常の関数)と非同期関数(async fn など)という “色の異なる” 関数が共存 する結果、次のような連鎖的制約が生じる。
- 呼び出し制限
- 同期関数からは非同期関数を直接呼び出せず、呼び出す側も非同期化(色が変わる)を迫られる。
- 色の伝播
- 非同期関数を上位レイヤへ持ち上げるたびに、その関数を呼び出すすべてのコードも async/await 対応を余儀なくされ、コードベース全体に“色”が伝播する。
- 境界コスト
- “同期 ↔ 非同期” の境界を跨ぐ箇所では実行コンテキスト(ランタイム、エグゼキュータ)を明示する必要があり、実装とテストの複雑度が上がる。
この「関数に色を付ける」という比喩は、Bob Nystrom 氏のブログ記事 What Color Is Your Function? に由来する。Rust でも async fn が「色付き」、通常の関数が「無色」とみなされるため、設計段階で非同期境界を慎重に引く必要がある。
リファクタリング(Refactoring)
型エラーが明確に示されるので、大規模なリファクタリングも「絵合わせ」のようにエラーを潰していけばよい点が魅力だとしている。
採用(Hiring)
Rustプログラマの採用は実は難しくないと述べている。Rustに興味をもつ人材は学習意欲が高いケースが多く、逆にPythonなどメジャー言語は応募者数が多すぎて精査が難しい面もあるという。
感情面(Affect)
筆者はPython + Djangoで感じていた「コードが壊れやすい」「パフォーマンスは大丈夫か」などの不安感がRustでは大幅に減り、堅牢性と美しさを両立できる充実感があるという。
The Bad
続いて、Rustの「悪い点」として筆者が挙げている部分をまとめる。
モジュールシステム(The Module System)
Rustではコンパイル単位が「クレート(crate)」であり、モジュール(module)は名前空間や可視性を管理するための仕組みにとどまる。大きなクレートはビルドが遅く、クレート分割は管理が煩雑になるなどのトレードオフがあると指摘している。
ビルド性能(Build Performance)
ビルド時間が長くなりがちなのはRustの最大の課題だと述べられている。LLVMのコンパイラバックエンドによる部分や単相化(monomorphization)など言語仕様上の問題もあるため、結局はリソースを増やして対応するのが手っ取り早いとまとめている。クレート分割やキャッシュ利用などの方法はあるが複雑で、最終的にはCIリソース増強が現実的とまとめる。
モック(Mocking)
依存関係を差し替えながらテストする仕組み(モック)をRustで実装するのは、型やライフタイムを明示的に扱う分だけ煩雑になると指摘されている。trait
と実装/モックを切り替える方法は動くが、記述量が多くなる例として以下のインタフェースが示されている。
trait InsertUser<T> {
fn execute(
&mut self,
tx: &T,
email: &Email,
password: &Password
) -> Result<(), CustomError>;
}
表現力(Expressive Power)
過剰なプロシージャルマクロやトレイトの濫用はコードベースを複雑化させかねず、コードの流れを追いづらくなる危険があると警鐘を鳴らしている。
以上が記事の概要である。Rustは堅牢性や高速性、優れたツールチェーンなど多くの利点がある一方、大規模開発ではビルド時間やモジュール分割、モックテストの煩雑さなど課題も存在することが示されている。
詳細はTwo Years of Rustを参照していただきたい。