- Rustはさまざまな概念が相互に緊密に絡み合った言語であり、基本的なプログラムを理解するためにも多くの要素を同時に学ぶ必要がある
- 関数、ジェネリクス、列挙型、パターンマッチング、トレイト、参照、所有権、
Send/Sync、Iterator などはすべて相互作用するように設計された中核要素である
- JavaScriptと比較すると、JSは一部の概念だけを理解していてもコードを書けるが、Rustは言語全体の文脈を理解してはじめて意味のあるコードを書ける
- Rustのこうした複雑さは学習のハードルを高める一方で、安全性と一貫性を提供し、コード設計のあり方に大きな影響を与える
- このような言語的な緻密さがRustを特別なものにしており、「より小さなRust」というビジョンは精巧に結び付いた言語哲学をあらためて見つめ直させる
Rust学習の難しさ
- Rustは参入障壁が高いにもかかわらず、多くの人がドキュメント、API、診断の改善に貢献してきた
- 基本概念としては、第一級関数、列挙型、パターンマッチング、ジェネリクス、トレイト、参照、借用チェッカー、並行性の安全性、イテレータなどがある
- これらの概念は互いに依存し合って絡み合っているため、一つずつ切り離して学ぶのが難しく、標準ライブラリもその大半でこれらの機能を活用している
- 20行前後のRustコードであっても理解するには、関数型パラダイム、
Result とエラー処理、ジェネリック型、列挙型、イテレータなど複数の要素を同時に把握しなければならない
RustとJavaScriptの比較
- 同じファイル変更検知プログラムをRustとJSで書いた場合、Rustでは多数の言語概念が絡み合っている
- JSでは基本的に関数と null 処理だけを理解していれば、十分に動作するコードを書ける
- これはRustが単により難しいという意味ではなく、Rustが言語全体の構造的理解を求める設計であることを示している
Rustの相互結合された設計
- Rustの核心は有機的に設計された機能同士の結合にある
- 列挙型はパターンマッチングなしでは扱いにくく、パターンマッチングも列挙型なしでは制約が大きい
Result と Iterator はジェネリクスなしでは実装できない
Send/Sync の概念と println の制約は、トレイトがあってはじめて安全に表現できる
- 借用チェッカーはクロージャのキャプチャ解析を通じて
Send/Sync の安全性を保証する
- このような相互結合によって、Rustは単なる機能の寄せ集めではなく、統合された言語体系になっている
小さなRustのビジョン
- 2019年にwithout.boatsは「Smaller Rust」に言及し、小さく洗練されたRustの可能性を議論した
- 今日のRustははるかに大きくなったが、小さなRustという概念は精巧にかみ合った言語設計の本質を思い出させてくれる
- Rustの魅力は、言語要素が互いに独立しつつも、結び付いたときに強力な表現力と安全性を提供する点にある
結論
- Rustは学ぶのが難しいが、相互に絡み合った概念の一貫性と統合性が大きな強みとして働いている
- この構造のおかげで、Rustは開発者に単にコードを書かせるだけでなく、安全性と性能を同時に考える思考様式を身に付けさせる
- Rustの本質は「小さく精巧な中核言語」にあり、これは今日の拡張されたRustにおいてもなお重要な哲学として残っている
1件のコメント
Hacker Newsの意見
fs.watchのドキュメントには、コールバックでfilenameがnullになり得るので必ずチェックすべきだと明記されている。Rustならこの事実が型システムに反映され、必ず処理させられるが、JSでは雑にコードを書きやすい。関連ドキュメントnullチェックが強制される。だからこれは、TSがJSにおいてRust的な正しさにもう少し近づく、比較的負担の少ない段階であることを示す良い例だと思うfor path in pathsはfor (const path of paths)であるべきだ。JSは括弧がないとすぐエラーになるが、inとofの違いは、値ではなくインデックス(iterable index)を走査することにある。結果として、実際にはインデックスが文字列に変換されてfs.watchの第1引数に渡されてしまう。しかもTypeScriptでもこのミスを検出できないことがあるkindがどこから来たのか気になる。console.log("${kind} ${filename}")ではkindではなくeventType(文字列)であるべきだprintlnはDisplayかDebugトレイトを実装した型しか出力できない。だからPathはそのままでは出力できない。すべてのOSがUTF-8に適合するパスを保存するわけではなく、Rustの文字列型はすべてUTF-8だからだ。つまりPathの出力は情報落ちを伴う可能性がある。PathはdisplayメソッドでDisplay実装型を返す。Rustはこれを型システムに組み込んでいるが、JS/TSでは内部文字列がUTF-16であることを明示しにくく、UnicodeでないパスはTextEncoder/TextDecoderを直接使わないと正しく扱えない。昔の経験では、サーバーがShift_JISでテキストを送ってきたのをresponse.text()で読んだら、実行時に空文字列しか返ってこなかった。エンコーディング問題に慣れていなければ、この状況のデバッグに何日も費やしかねない。 そしてJSの例には、Rustコードにはないバグや構文エラーがある(ループではfor-inではなくfor-ofが必要)。この例を「第一級関数」だけで済むとは言いにくく、Rustのようにイテレータの理解も必要で、CommonJSも使っている。さらにasync/await、Promises、top-level await も新たに学ばなければならず、top-level await は node を含む一部ランタイムで最近になってようやくサポートされたものだ。今でも一部のJSエンジン(例: React NativeのHermes)では未対応だこういう点こそが、私がRustを使い続ける理由だ。例は1つにすぎないが、こうした細かな問題や落とし穴は他の言語には常に散らばっている。個別には起きないかもしれないが、プログラム全体のライフサイクルで積み重なると、どこからともなく変なバグが出続けて、それを延々と探し回ることになる。Rustではこういうことが起きない。型システムが信じられないほど多くのケースを事前に防いでくれる。実際、Rustで機能を全部作ったソフトウェアをリリースしてからは、たまに機能追加をしただけで、一般的なバグ修正の苦労がほとんど消えた。もちろん論理バグはどこでも起こり得るが、他の言語のような愚かな型/構造の不一致に由来する問題を元から遮断してくれるので、生産性と保守性がまったく違う体験になる
個人的には、JS/TSで thenable/Promise と async-await を本当に正しく理解している開発者はあまり多くないと感じる。こんなのも見たことがある。
コールバック形式のラッパーをそのままPromiseで包み、それを async 関数の中でまた使っている。こういうのを見るたびにつらくなる。実際、こういうコードはあちこちで見かける。 さらに、モジュールの import や async
import()、トランスパイル、コード分割まで考えると本当に複雑だrustfmt、rust-analyzer周りの細部と、rustcのバグ修正、およびCargoのエラーレポート改善だ。私自身、毎日cargo scriptで課題再現用スクリプトを書いている-Zscript機能のキーワード検索を始めて調べていたら気が散ったという話だ。2023年から進んでいて、ほぼ完成に近く見えるオープンIssueもある。ZomboDBのリポジトリでもやはりrustでビルドパイプラインを処理しているのを見たが、全体の文脈までは完全には理解していない。 cargoフロントマターがスクリプトの移植性で非常に有用だという点も触れておきたい。ファイル1つを共有するだけでよく、PythonやNode.jsのように追加インストールや初期化なしですぐ依存関係を取得して使える#!/some/pathで始まるファイルは、シェルが指定されたコマンドにファイル全体をstdinとして渡して実行するだけだasyncとconstくらいだ。だとすれば「asyncとconstが入る前のRustのほうが小さくてきれいだった」と言ってくれればよいのに、本文ではそこまで直接的に説明されていないのが惜しいCopytrait、reborrowing、deref coercion、ループでの自動into_iter、スコープ終了時の自動drop呼び出し(これも明示呼び出しにするかコンパイラがエラーにしてもいい)、trait boundにおけるデフォルトの:Sized、ライフタイム省略(lifetime elision)、match ergonomics など、さまざまな自動化や利便機能を全部外せば、本当に機械的に単純なRustは可能だ。しかしそんな言語は日常的に使うには非常に不便だろう。皮肉なことに、これらの要素は実は初心者のために設計されたものでもあるasyncとconst導入前のほうが小さくてきれいだったということだ。それを率直に言わなかったのは、その機能の開発者に友人が多いからでもある。Matkladが lobste.rs で非常にうまく表現している。2015年のRustのほうが完成度が高く一貫性もあったが、Rustのビジョンは完全な一貫性(coherence)ではなく、産業で役に立つ言語になることだ.into()とFromtrait は型変換をあまりにも暗黙的に処理する。標準ライブラリにもこうした「便利」関数が多い。その結果、オブジェクトの型が曖昧になり、関数呼び出しと実装の対応関係が追いにくい(もちろんIDEが助けてくれれば多少はましだが)?演算子もあまり好きではないconstキーワードの意味のように、Rustで学ぶことが、後で既存言語で身につけた悪い癖を矯正する手間を減らしてくれる面もあるmemモジュールのパターンに従っているので、インターフェース構造をしっかり理解したいなら std::mem から始めるのがよい