11月24日、bykozyが「Blog - Rust is a disappointment」と題した記事を公開した。この記事では、Rustが期待されてきた「C++の後継候補」としての役割を十分に果たしていないのではないかという問題提起を行い、とくにコンパイル速度の遅さ、言語としての複雑さ、メモリ安全性と信頼性のトレードオフ、可変共有状態の扱いにおける限界について詳しく紹介している。
以下に、その内容を紹介する。
C++もRustも好きではない、という立場
筆者は冒頭で、自分はかつて「Rustヘイター」を自称していたが、それはRustがStack Overflow調査などで「最も愛されている言語」として持ち上げられる状況への反発からだったと説明する。C++に対しても多くの不満があり、C++そのものを嫌っていることも明言している。
多くの開発者が「C++の代わりになる、より良いプログラミング言語」を待っていたが、実際に登場したのはRustであり、それは理想像とはかなり違っていた、というのが筆者の基本的なトーンである。
筆者はRustの中核的な問題として、次の4点を挙げる。
- コンパイルが非常に遅く、本質的に速くなりにくい。
- 言語としてC++並みに複雑であり、所有権やライフタイムの管理が常に開発者の負担になる。
- メモリ安全性を過剰に優先した結果、実用上の信頼性や開発者の「正気」が犠牲になっている。
- GUIやDB、大規模な状態管理など「大量の可変共有状態」を扱う用途では、設計・実装ともに向いていない。
そのうえで、「Rustが良いか悪いかではなく、膨大な工数が投じられた結果、棚から取ってそのまま使える“そこそこの言語(mediocre)”に落ち着いているに過ぎない」という評価を示す。
「C++もひどい」という前提
Rust批判に入る前に、筆者はまずC++そのものの問題を整理する。とくに、未定義動作(Undefined Behavior; UB)が言語の根幹に埋め込まれている点を強く批判している。
配列アクセス一つとっても、範囲外アクセスはチェックされず、すぐにUBに至る。しかも、多くのUBはパフォーマンスのためというより「C言語時代のずさんな設計がC++に持ち込まれ、さらに増幅された結果だ」と述べる。
C++に関する具体的な問題点として、記事では次のような箇条書きが挙げられている。
- 暗黙の型変換、暗黙のコピー、暗黙のコンストラクタ呼び出し、暗黙のオブジェクトスライシングなど、とにかくあらゆるものが「implicit」であること。
- とくにSTLにおける関数オーバーロードの乱用によって、挙動の把握が難しいこと。
- 例外処理が後付けで導入されたため、エラー処理のスタイルが一貫していないこと。
- 40年経った現在もなお、
#includeによるテキストインクルードと、十分に検査されないOne Definition Ruleに依存していること。 - 複数のパラダイムを無理に混在させた結果、継承クラスでジェネリック関数を素直にオーバーライドすることさえ困難であること。
- SFINAE(Substitution Failure Is Not An Error)のようなトリッキーな仕組みが、ジェネリックプログラミングの中核メカニズムになってしまっていること。
T,T&,T*,std::optional<T>,std::unique_ptr<T>など似た概念を表す型が乱立し、それぞれに独自の罠があり、その上にconstまで乗ってくること。
このように、C++は複雑で安全ではなく、コンパイラも遅い。では、Rustはこれらをどこまで「解決できているのか」というのが、記事全体の問題設定である。
1. コンパイルが本質的に遅い
ジェネリクスと借用チェッカの組み合わせがボトルネック
筆者は、Rustコンパイルの遅さは一時的な問題ではなく、言語設計そのものに起因する本質的な問題だと主張する。Rustのコンパイルモデルについて解説したMedium記事や、Rust FAQでの説明を引用しながら、以下のような構造的理由を挙げる。
Rustはジェネリクスを多用する言語であり、Haskellやテンプレート多用のC++と同様、モノモーフィゼーション(型ごとのコード生成)によってコンパイルコストが増大する。
例えば、単純なループも
for i in 0..limit {}のように書くと、範囲オブジェクトを生成し、イテレータを生成し、そのイテレータを回すという複数の抽象化が積み重なる。それぞれが具体的な型にモノモーフィックに展開され、個別に最適化される必要がある。
最適化なしのRustコードは「デバッグ用途ですら使い物にならないほど遅い」とし、実用的な性能を得るには重い最適化パスを回すしかないと述べる。
ここに、非オプションな借用チェッカが上乗せされる。借用チェックによってコンパイル時の解析コストがさらに増え、コンパイルエラーを潰すたびに再コンパイルを繰り返すことになるため、「遅いコンパイル」が日常的な開発体験を直撃する、という指摘である。
Rustコンパイラは年々高速化してきたが、「二倍速くなった程度では足りず、本来必要なのは“二桁オーダーでの高速化”だ」というのが筆者の評価だ。
2. 高い複雑さと所有権モデル
「高レベルロジックだけ書きたい」という逃げ道がない
次に筆者は、Rustの複雑さそのものを問題視する。とくに、つぎのような点を挙げる。
- Rustにはガーベジコレクタがなく、「今後も導入される可能性はない」と見なしている。
- そのため、開発者はあらゆるデータ構造を所有権の木(ツリー)として“半手動”でパックする必要がある。
- ライフタイム、所有権、借用、トレイトといった概念に精通していなければ、数行のコードを書くことすらままならない。
筆者は、Arc<Mutex<Box<T>>>のような型があちこちに現れるコードベースを「ジャングル」と表現し、その複雑さがロジックの可読性・保守性を直接下げていると指摘する。木を見て森を見失う状態、というわけである。
また、こうした複雑さとコンパイルの遅さが重なった結果として、初期のRust採用者の多くが、性能がそれほどシビアでないサービスについてはNode.jsやGoに戻っていったと述べている。高レベルな業務ロジックを素早く書きたい場面では、Rustは実務上扱いづらいという評価だ。
3. メモリ安全性 vs. 実用上の信頼性
「Rustはメモリ安全で、しかし信頼性が低い」という主張
Rustの根本には、性能とメモリ安全性という二つの“妥協しない”目標が置かれている。筆者は、このうち「メモリ安全性」を優先しすぎた結果、以下を失ってしまったと主張する。
- 開発者の正気(sanity)
- システムとしての実用的な信頼性(reliability)
例えば、標準ライブラリのコンテナ類は内部でunsafeを使って実装されており、「完全な正しさはチューリングマシンのモデル上そもそも達成できない。安全なプログラムは、注意深く設計された“unsafeな部品”を組み合わせて作るしかない」という前提がある、と指摘する。
一方で、Node.jsやGoは「実用上は十分に安全な言語」とみなされることが多いのに対し、Rustはメモリ安全性を徹底しようとした結果、現場の実務や精神的負荷の面では必ずしも優れているとは言えないと批判する。
「クラッシュより誤動作がマシ」という文脈
筆者はさらに、「多くのユースケースでは“クラッシュしないこと”のほうが重要であり、多少の誤動作であっても、リモートコード実行や秘密データ漏洩に至らないなら許容されうる」という考えも提示する。
例として、心臓ペースメーカーのような機器において、プロセスがクラッシュして停止するくらいなら、多少挙動がおかしくても動作し続けるほうがマシという極端な例を挙げる。また、Cloudflareで発生したunwrap()呼び出しによるクラッシュと障害の事例を引きつつ、「ここではクラッシュが信頼性を損なう要因になっている」と論じる。
そのうえで、筆者は次のようにまとめる。
Rustはメモリ安全だが信頼性が低い。メモリ安全性の代償として、信頼性と開発者の正気が支払われた。
そしてこの点こそが、筆者にとって最も強い不満点だと位置づけている。
4. 可変共有状態とRustの相性の悪さ
Rustが得意とするのは「一方向・イミュータブル」な世界
筆者は、大量の可変共有状態(mutable shared state)を扱うケースでRustがうまく機能しないと主張する。ここでいう可変共有状態には、GUI、データベース、大規模なステートフルサービス、OSやハードウェアに近い層などが含まれる。
一方で、Rustが成功している領域として、次のような例を挙げる。
- Rustコンパイラ
rustc mdbookやpulldown-cmarkといったMarkdownツール- ActixやAxumによるステートレスなHTTPハンドラ
- 追記専用のブロックチェーン
- シングルスレッドのWebAssembly
これらはいずれも、共有状態が読み取り専用であったり、一方向のデータフローであったり、非循環なデータ構造に基づいているという共通点がある。筆者はここにもHaskellとの類似性を見出している。
共有状態を本格的に扱うと、Rustの強みを失う
問題は、ここから共有可変状態の世界に踏み込んだときだと筆者は述べる。
- 共有可変状態では、メモリ破損が「例外」ではなく「前提ルール」になる。そのため、単純にクラッシュするのではなく、破損を検知・処理し続けねばならない。
- 循環グラフ構造に対して所有権を静的解析することは難しく、実質的にGCライクなアルゴリズムが必要になる。
Sync/SendトレイトやMutex、参照カウント(Arc)などで共有状態を表現しようとすると、ロックやCPUキャッシュへの悪影響により、マルチスレッド通信の性能が著しく低下する。
つまり、可変共有状態を本気で扱おうとすると、Rustの売りである性能と安全性の両方を取り逃がし、コンパイルの遅さと複雑さだけが残る、と筆者は結論づける。
具体例として、長らくベータ版で開発が続いているRust製のIDE「Zed」に触れ、「可変状態だらけのGUIを借用チェッカのジャングルの中で実装する開発者の苦労が目に浮かぶ」と述べる。大規模なGUIアプリケーションや本格的なデータベース、大規模なステートフルサービス、主要なLinuxカーネルモジュールなど、Rustが“本命”として使われている例はまだほとんど見られない、とも指摘している。
最終的な評価:「悪くはないが、最高でもない」
記事の最後で、筆者はRustの総評をあらためて整理する。
- Rustは良くも悪くも「並のプログラミング言語」であり、「失望(disappointment)」というタイトルは、C++の後継として期待された高さとのギャップを指している。
- しかし、数千人月レベルの開発投資が行われてきた実績や、エコシステムの厚みによって、「棚から取ってきてそのまま利用できるツール」としての価値はある。
- 実際に、このブログ自体はRust製の静的サイトジェネレータZolaで生成されており、筆者はRustコードを書かずにその恩恵を受けている。
- Zolaのような「非対話的で、一方向のイミュータブルなデータフロー」に基づくツールに対しては、Rustは適した選択肢になりうる。
一方で、筆者は最後に次のようなメッセージを残す。「Rustは使う価値のあるツールではあるが、『最高のプログラミング言語だから、みんなRustに乗り換えるべきだ』と叫ぶのはやめてほしい」というものである。
詳細はBlog - Rust is a disappointmentを参照していただきたい。