36 ポイント 投稿者 GN⁺ 2025-08-23 | 1件のコメント | WhatsAppで共有
  • Rustはさまざまな概念が相互に緊密に絡み合った言語であり、基本的なプログラムを理解するためにも多くの要素を同時に学ぶ必要がある
  • 関数、ジェネリクス、列挙型、パターンマッチング、トレイト、参照、所有権、Send/SyncIterator などはすべて相互作用するように設計された中核要素である
  • JavaScriptと比較すると、JSは一部の概念だけを理解していてもコードを書けるが、Rustは言語全体の文脈を理解してはじめて意味のあるコードを書ける
  • Rustのこうした複雑さは学習のハードルを高める一方で、安全性と一貫性を提供し、コード設計のあり方に大きな影響を与える
  • このような言語的な緻密さがRustを特別なものにしており、「より小さなRust」というビジョンは精巧に結び付いた言語哲学をあらためて見つめ直させる

Rust学習の難しさ

  • Rustは参入障壁が高いにもかかわらず、多くの人がドキュメント、API、診断の改善に貢献してきた
  • 基本概念としては、第一級関数、列挙型、パターンマッチング、ジェネリクス、トレイト、参照、借用チェッカー、並行性の安全性、イテレータなどがある
  • これらの概念は互いに依存し合って絡み合っているため、一つずつ切り離して学ぶのが難しく、標準ライブラリもその大半でこれらの機能を活用している
  • 20行前後のRustコードであっても理解するには、関数型パラダイム、Result とエラー処理、ジェネリック型、列挙型、イテレータなど複数の要素を同時に把握しなければならない

RustとJavaScriptの比較

  • 同じファイル変更検知プログラムをRustとJSで書いた場合、Rustでは多数の言語概念が絡み合っている
  • JSでは基本的に関数と null 処理だけを理解していれば、十分に動作するコードを書ける
  • これはRustが単により難しいという意味ではなく、Rustが言語全体の構造的理解を求める設計であることを示している

Rustの相互結合された設計

  • Rustの核心は有機的に設計された機能同士の結合にある
    • 列挙型はパターンマッチングなしでは扱いにくく、パターンマッチングも列挙型なしでは制約が大きい
    • ResultIterator はジェネリクスなしでは実装できない
    • Send/Sync の概念と println の制約は、トレイトがあってはじめて安全に表現できる
    • 借用チェッカーはクロージャのキャプチャ解析を通じて Send/Sync の安全性を保証する
    広告
  • このような相互結合によって、Rustは単なる機能の寄せ集めではなく、統合された言語体系になっている

小さなRustのビジョン

  • 2019年にwithout.boatsは「Smaller Rust」に言及し、小さく洗練されたRustの可能性を議論した
  • 今日のRustははるかに大きくなったが、小さなRustという概念は精巧にかみ合った言語設計の本質を思い出させてくれる
  • Rustの魅力は、言語要素が互いに独立しつつも、結び付いたときに強力な表現力と安全性を提供する点にある

結論

  • Rustは学ぶのが難しいが、相互に絡み合った概念の一貫性と統合性が大きな強みとして働いている
  • この構造のおかげで、Rustは開発者に単にコードを書かせるだけでなく、安全性と性能を同時に考える思考様式を身に付けさせる
  • Rustの本質は「小さく精巧な中核言語」にあり、これは今日の拡張されたRustにおいてもなお重要な哲学として残っている

1件のコメント

 
GN⁺ 2025-08-23
Hacker Newsの意見
  • 「単純な」JSプログラムにもバグがあるのは皮肉だと感じる。fs.watch のドキュメントには、コールバックで filenamenull になり得るので必ずチェックすべきだと明記されている。Rustならこの事実が型システムに反映され、必ず処理させられるが、JSでは雑にコードを書きやすい。関連ドキュメント
    • TypeScriptを使えば null チェックが強制される。だからこれは、TSがJSにおいてRust的な正しさにもう少し近づく、比較的負担の少ない段階であることを示す良い例だと思う
    • 追加のバグもある。for path in pathsfor (const path of paths) であるべきだ。JSは括弧がないとすぐエラーになるが、inof の違いは、値ではなくインデックス(iterable index)を走査することにある。結果として、実際にはインデックスが文字列に変換されて fs.watch の第1引数に渡されてしまう。しかもTypeScriptでもこのミスを検出できないことがある
    • 指摘のとおりループ構文自体が不正確で、実行してみればすぐ分かる。つまり、筆者はそのJSコードを注意して書いたわけではなく、論点として大した意味はなかったと考えるほうがよさそうだ
    • 私が見落としたのかもしれないが、kind がどこから来たのか気になる。console.log("${kind} ${filename}") では kind ではなく eventType(文字列)であるべきだ
  • 細かい指摘を1つしたい。Rustの printlnDisplayDebug トレイトを実装した型しか出力できない。だから Path はそのままでは出力できない。すべてのOSがUTF-8に適合するパスを保存するわけではなく、Rustの文字列型はすべてUTF-8だからだ。つまり Path の出力は情報落ちを伴う可能性がある。Pathdisplay メソッドで 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 を本当に正しく理解している開発者はあまり多くないと感じる。こんなのも見たことがある。

      var fn = async (param) => new Promise((res, rej) => {
        fooLibraryCall(param).then(res).catch(rej);
      });
      

      コールバック形式のラッパーをそのままPromiseで包み、それを async 関数の中でまた使っている。こういうのを見るたびにつらくなる。実際、こういうコードはあちこちで見かける。 さらに、モジュールの import や async import()、トランスパイル、コード分割まで考えると本当に複雑だ

  • Bjarneの引用は、実際にはC++がどんどん悪くなっていくことを繰り返し正当化するためのセールストークだと思う。初期には本気だったのかもしれないが、今ではパターンが繰り返されている。構造はこうだと思う:
    1. 「C++の中には、もっと小さくてきれいな言語がある」
    2. しかし言語をsubsetとして取り出すことはできないので、まずsuperset(多機能化)を作って、その後でsubset化しようと言う
    3. supersetは新しいC++N+1に入る。本当のsubsetの議論はその次にすると先送りし、これを繰り返す
    4. C++N+1はさらに複雑になり、こうして永遠に繰り返される 何度もこれを見てきた人が、なぜまだそこに留まっているのか理解できない。結局「もっと小さくてきれいな言語」は決して現れない。いつもステップ1を繰り返しているだけだ
    • xkcd 927を連想した。xkcd 927。C++標準は毎回ますます複雑になり、良い変化もあるが既存バージョンと噛み合わないこともあり、ソースコードはどんどんひどくなる。2つのOSSライブラリを管理しているが、今ではほとんど使っていない。いつまで耐えるべきか最近悩んでいる。 Rustはc++11/14/17/20から移ってきて本当に新鮮だ。ただしRustも全体を把握しようとすると十分に巨大だ。今回の記事の指摘はとても的確だと感じる
  • shebang(自己実行型rustスクリプト)を見た瞬間に気が散った人はいない? 昔Goで同じものを見つけたときのように驚いた。かなり便利そうで、基本的な用途なら十分使えそうだ。rustでビルド/テストパイプラインを管理するプロジェクトでも似たものを見たことがある。こういう用途ではかなり良い代替になるだろう。 ただ、私はたいていbashから少しでも外れるスクリプトが必要ならDeno+TSを使う。JSを最も長く(28年間)扱ってきて、その次がC#で24年だ。Nodeも初期から使っている。Denoはパッケージ共有/集中管理の面でNodeやPythonより管理しやすい。cargoフロントマターも似たように動作する
    • cargoへのscript統合を自分で設計・実装した者だ(これまでにもサードパーティ実装は多くあった)。実運用の事例を見るのは本当にうれしいし、言及されているのを確認できてよかった。ドキュメントも参照してほしい。どんな形が適切か、言語とどう連携するか、最初のリリース範囲をどこまでにするかなど、長い議論があった。今はスタイルガイドやRustリファレンスの更新など仕上げ作業をしていて、残る大きな仕事は rustfmtrust-analyzer 周りの細部と、rustc のバグ修正、およびCargoのエラーレポート改善だ。私自身、毎日cargo scriptで課題再現用スクリプトを書いている
    • 実際に -Zscript 機能のキーワード検索を始めて調べていたら気が散ったという話だ。2023年から進んでいて、ほぼ完成に近く見えるオープンIssueもある。ZomboDBのリポジトリでもやはりrustでビルドパイプラインを処理しているのを見たが、全体の文脈までは完全には理解していない。 cargoフロントマターがスクリプトの移植性で非常に有用だという点も触れておきたい。ファイル1つを共有するだけでよく、PythonやNode.jsのように追加インストールや初期化なしですぐ依存関係を取得して使える
    • Goでも同じことができると言っていたが、詳しく説明してもらえるだろうか。関連リンクなら私も興味がある
    • JSとC#を長く使ってきた立場だが、2025年にそういう理由でシステムを選ぶのはあまりよくないと思う。この20年で本当に多くのものがずっと良くなった
    • 単なる基本的なUnixの機能だ。#!/some/path で始まるファイルは、シェルが指定されたコマンドにファイル全体をstdinとして渡して実行するだけだ
  • Rustで「内側からもっと小さくてきれいな言語が出てきている」と言うとき、その言語が具体的に何なのか気になる。記事を読むと、参照、ライフタイム、トレイト、列挙型などは全部残っていないと成立しないという意味に見え、そうなるとほとんどRustと変わらない。最後のパートで「使いたいRust」と「過去のRust」という2つのヒントが出てくるが、あまりしっくりこない。withoutboatsの "Notes on a smaller Rust" も読んだが、設計目標自体がRustとは違うので、Rustになろうとしているのではなく、新しい言語設計を考えるときにRustから得られる教訓を示している程度だ。Rustを目指す言語ではなく、「メインストリーム」の要請に合わせた言語の例(例: GC、コンパイル/文法の単純化など)だ。第二に、「2018年に初めて学んだときに恋に落ちた言語がその『より小さいRust』だ」という話も出てくるが、実際には2018年以降のRustは本質的にはあまり変わっていない。editionの変更など大半は文法上の柔軟性向上で、本当に大きな例外は asyncconst くらいだ。だとすれば「asyncとconstが入る前のRustのほうが小さくてきれいだった」と言ってくれればよいのに、本文ではそこまで直接的に説明されていないのが惜しい
    • 「より小さくてきれいなRust」を言うなら、Austral 言語が例として思い浮かぶ
    • Rustの核心概念を保ちながら、より単純な(より小さい)言語は可能だという主張もある。たとえば、Copy trait、reborrowing、deref coercion、ループでの自動 into_iter、スコープ終了時の自動 drop 呼び出し(これも明示呼び出しにするかコンパイラがエラーにしてもいい)、trait boundにおけるデフォルトの :Sized、ライフタイム省略(lifetime elision)、match ergonomics など、さまざまな自動化や利便機能を全部外せば、本当に機械的に単純なRustは可能だ。しかしそんな言語は日常的に使うには非常に不便だろう。皮肉なことに、これらの要素は実は初心者のために設計されたものでもある
    • かなり注意深く読んでくれている。実際、私の意図も、Rustは asyncconst 導入前のほうが小さくてきれいだったということだ。それを率直に言わなかったのは、その機能の開発者に友人が多いからでもある。Matkladが lobste.rs で非常にうまく表現している。2015年のRustのほうが完成度が高く一貫性もあったが、Rustのビジョンは完全な一貫性(coherence)ではなく、産業で役に立つ言語になることだ
  • 偏見はあるかもしれないが、Rustは最も完全に近い言語だと思う。borrow checker は煩わしいが必要不可欠だ。同じようにバグのあるコードがCだったなら、実行時に破綻していただろう――結局そのときもバグは直さなければならない。違いは、Rustではコンパイル前にそのバグを解決するよう強制されるが、Cでは真夜中の障害対応になることだ。Rustは難しいというより、別の考え方への転換を求める。安全でセキュアなコードを書くというパラダイムシフトが必要なのだ。変化はたいてい不快なものだが、それがRustへの拒否感の根本原因のように思う
    • Rustは完璧には程遠い
      • Derefの適用タイミングと順序をコンパイラが自由に決めすぎていると思う。.into()From trait は型変換をあまりにも暗黙的に処理する。標準ライブラリにもこうした「便利」関数が多い。その結果、オブジェクトの型が曖昧になり、関数呼び出しと実装の対応関係が追いにくい(もちろんIDEが助けてくれれば多少はましだが)
      • 暗黙のreturnはプログラムの流れを見えにくくしてミスを招く。? 演算子もあまり好きではない
      • 小さなRustモジュールに細かく分かれすぎていて、何か有用なことをしようとすると数百個の依存関係が必要になる。それぞれ別々に管理してvendorしないと安定したビルドにならず、本当に不便だ
      • Async Rustは今や完全に混沌としている
    • borrow checker 自体への不満というより、Rustそのものの「塊」が大きくなりすぎたというのが主な指摘だ。2018年の粗削りで不完全なRustが好きだった人たち(私も含む)にとっては、今はあまり魅力的ではない。もちろん熟達して使えば非常に強力だが、本当にそれだけの努力を払う価値があるのかと自問してしまう。2025年にC/C++の代替を選ぶならZigを選ぶと思う(唯一の例外はPostgres作業で、pgrxエコシステムは非常に突出している)。それでもCで働くよりは何でもましだ
  • 初めて学ぶ言語としてRustを勧めるべきではないと思う。最初の言語を学ぶだけでも大変なのに、Rustはコンパイラエラーのせいでコードが完全に正しくなるまで実行すら見られないことが多い。あまりに挫折しやすく、簡単に諦めてしまうだろう。Python、JavaScript、Luaあたりから始めて、ゲームのようなものを素早く作って試行錯誤するほうがいいと勧めたい
    • 私の経験は違う。うちの会社のMLエンジニアはPythonしか知らなかったが、Rustコードベースに貢献したいと言うので、私が1時間ほど基礎を説明したらすぐ適応して、生産性もすぐ上がった。実際、ゲームを作っていて文字列を数値関数に渡してクラッシュすると、原因を追うだけで時間が消える。Rustならコンパイラが最初から「ここはstringだがintであるべきだ」と示してくれるので、むしろデバッグはすぐ終わる。1日中コンパイルエラーに向き合う代わりに、実行時エラーで1週間苦しまずに済む
    • ブログ冒頭の引用の本人だ。私はRustを最初の言語として400人以上に教えてきたが、このスレッドの主張はとても興味深い。長期間の実体験を通して、可能性だけでなく、かなりうまく機能するという十分な根拠も得ている
    • まだ確信は持てない。優れた教育者がRustを最初の言語として教えるのを見てみたい。世代が変わって大学でもPythonが多く使われているが、理論的にはRustが最初の言語としてコホート全体の水準を引き上げる可能性もあると思う(もちろん fail rate が高すぎて運営上の問題になるかもしれないし、逆に上級学生はもっと多くを学べるかもしれない)。move assignment や const キーワードの意味のように、Rustで学ぶことが、後で既存言語で身につけた悪い癖を矯正する手間を減らしてくれる面もある
    • 普通は最初の言語として静的型付けは避けたほうがいいと勧めたい。私自身は静的型付けが好きだが、初心者の立場では余計に混乱が増えるだけだ。コンパイラエラーはたいてい反事実的で、「コンパイラはこれがnoneではないと証明できなかった」といったメッセージは、テストケースで実行時クラッシュが起きてその場で位置を見つけるより、ずっと難しく感じられる。1行ずつ値を出力してトラブルシュートすれば大抵すぐ解決できるのに、コンパイラの難解なエラーに詰まると本当に長く迷うことがある
    • Rustは、一度にすべてを受け入れられるなら悪い言語ではない。問題は、誰もそんなふうに言語を学ばないことにあり、主要な概念を十分に理解していないと、Rustでは繰り返し試行錯誤することになる。そして結局、他の言語ではまったく学ばない概念も多いので、新しい言語へ移るときに再び挫折するかもしれない
  • 私が見た「シンプルなRust」に最も近い例は Gleam だ。かなりRustに影響を受けているように見える
    • GleamがRustに影響を受けたというのは誤解だ。作者は公式にはそう言っていない。コンパイラはrustで書かれているが、Gleamはパラダイムも対象ランタイムもまったく異なるので、Rustの代替ではない
    • 別の「simple rust」スタイルが欲しいなら、fsharpを見るのも勧めたい
    • Gleamのメインページに黒人の権利、トランスの権利、反ナチのメッセージがあって、私はこの言語にまったく興味がない
    • Gleamで3Dを作れるのか気になる
  • 「Rustプログラムが何をするのか説明していない」点が気になった。ものすごい技術的説明があるのに、プログラムが実際に何をするのかの要約がない。実際にはファイル変更を監視して表示するだけだ。Rustではこういう単純な作業でも実装は複雑になり、実際の問題とは関係ない内部の細部まで気にしなければならないという点が、この言語の難しさをよく示している。この複雑さこそが直面する課題であり、同時に自ら作り出した障壁でもある、という見方だ
    • 他の言語にも同じ問題はあるが、Rustはそれを先回りして扱えるようにしてくれる。すべてのファイル名が表示可能なわけではなく、多くの言語はこの部分をユーザーに丸投げしている。Rustは戻り値の型でエラー/失敗を明確に示すが、他の言語では例外処理のような別の仕組みが必要になる。一見単純に見えても、実はRustのほうが直感的かもしれない
    • 実装も、高性能言語としては実際かなり単純なほうだ。1ページの中に全部収まっている。これで十分単純な例ではないだろうか?
    • 単純な説明 = 単純な実装、とは限らない。XKCD 1425に良い例がある。(例: 写真が国立公園の中で撮られたか確認するのは簡単だが、それが鳥の写真かまで識別するには研究チームが必要になる) xkcd 1425
  • Rustは意味論的にかなり一貫していて凝集的だと感じる。他の言語に比べて糖衣的なものが少なく、そのぶん直感的だ。すべてのインターフェースはたいてい mem モジュールのパターンに従っているので、インターフェース構造をしっかり理解したいなら std::mem から始めるのがよい