30 ポイント 投稿者 GN⁺ 2025-12-06 | 1件のコメント | WhatsAppで共有
  • 3つの言語の哲学と価値観の違いを軸に、それぞれの言語がどのような問題を解決しようとしているのかを比較
  • Goは単純さと安定性を重視し、機能を最小限に抑えることで協業と保守を容易にする言語として説明される
  • Rustは安全性と性能の両立を目指し、複雑な型システムとトレイト構造によってメモリ安全性を保証
  • Zigは手動メモリ管理とデータ中心設計を通じて、開発者に完全な制御権を与える実験的な言語として描かれる
  • 3つの言語の対照的なアプローチは、プログラミング言語が実装する価値体系を浮かび上がらせ、開発者がどの哲学に共感するかが選択基準になることを示す

言語比較の観点

  • 筆者は仕事で使う言語ではなく、新しい言語の実験を通じて各言語の価値体系を理解しようとしている
  • 単純に機能一覧を比較するのではなく、言語がどのようなトレードオフを選んだかが重要だと強調
  • Go、Rust、Zigは機能的に重なる部分が多いが、設計者が重視した価値は異なる
  • 各言語の哲学を把握することで、どの環境や目的に適しているかを判断できる

Go — 単純さと協業を中心に据えた言語

  • Goはミニマリズムによって際立っており、「頭の中に言語全体を収められる」という特徴を持つ
    • ジェネリクスは12年後に追加され、タグ付きユニオンエラー処理のシンタックスシュガーのような機能は今も存在しない
  • 機能追加には非常に慎重で、ボイラープレートコードは多いが、言語の安定性と可読性は高い
  • Goのスライス(slice) はRustの Vec<T> やZigの ArrayList の機能を包括しており、メモリ位置はランタイムが自動管理する
  • C++の複雑さとコンパイルの遅さへの不満から出発し、単純で高速なコンパイルを目標に設計された
  • 企業環境での協業効率を重視し、複雑な機能よりも明快なコードと一貫性を優先する

Rust — 複雑だが強力な安全性と性能

  • Rustは**「ゼロコスト抽象化」を掲げ、多様な概念が結び付いたマキシマリストな言語**である
  • 学習難易度が高いのは概念密度が高いためであり、複雑な型システムとトレイト構造が存在する
  • Rustの中核目標は性能とメモリ安全性の両立にある
    • UB(Undefined Behavior) を防ぐため、コンパイル時に検証を行う
    • 誤ったポインタ参照や二重解放などによる予測不能な動作を防止する
  • コンパイラがコードのランタイム動作を理解できるように、開発者は型とトレイトを明示的に定義する必要がある
  • この構造のおかげで他人のコードに対する信頼性が高く、ライブラリエコシステムも活発に維持されている

Zig — 完全な制御とデータ中心設計

  • Zigは3つの言語の中で最も新しい言語で、バージョン0.14の段階にあり、標準ライブラリの文書化はほとんどない
  • 手動メモリ管理を採用しており、開発者は自分で alloc() を呼び出し、アロケータ(allocator) を選ばなければならない
  • RustやGoと異なり、グローバル変数の作成が簡単で、ランタイムで「illegal behavior」を検出するとプログラムを停止する
    • ビルド時に選択できる4種類のリリースモードで、性能と安定性のバランスを調整できる
  • オブジェクト指向プログラミング(OOP) の機能を意図的に排除している
    • privateフィールド動的ディスパッチはなく、std.mem.Allocator でさえインターフェースとして実装されていない
    • その代わりにデータ中心設計(data-oriented design) を志向する
  • メモリ管理も、RAII方式の細かなオブジェクト単位管理ではなく、大きなメモリブロックを定期的に確保・解放する構造を推奨する
  • Zigは自由で反体制的な性向を持つ言語として描かれ、OOP的な思考を取り除いて開発者主導の制御を最大化する
  • 現在チームはすべての依存関係の書き直し作業に集中しており、安定版(1.0)はまだ未定である

結論 — 言語が示す価値の違い

  • Goは協業と単純さ、Rustは安全性と性能、Zigは自由と制御権を中核価値としている
  • 3つの言語の違いは単なる機能比較ではなく、ソフトウェア開発に対する哲学的選択を反映している
  • 開発者は自分がどの価値に共感するかによって言語を選ぶことになる

1件のコメント

 
GN⁺ 2025-12-06
Hacker Newsの意見
  • Rustでmutable global variableを作るのは難しくない
    ただし unsafe や同期を提供するスマートポインタを使う必要がある
    Rustは基本的にre-entrantであり、コンパイル時にスレッド安全性を保証するからだ
    もし静的なスレッド安全性を気にしないなら、ZigやCのように簡単に作れる
    違いは、Rustがコードのランタイム動作についてより多くの保証手段を提供する点にある

    • 数年にわたってRustを使ってきた立場からすると、mutable global variableは「できるからといって、やるべきとは限らない」の典型例だと思う
      他の言語に戻って、こういうものを平然と使っているのを見ると、安全性の面では狂気じみているように感じる
    • 「trivialだ、ただ〜が必要なだけだ」という言い方は、C++やPerl、Haskellでも聞いたことがある
      しかし、そうした「単純なこと」が積み重なると、決して単純ではなくなる
      Rustはすでにその線を越えていて、もはやtrivialではない
    • Rustコンパイラがスレッド間のrace conditionをコンパイル時に検出してくれるのか気になる
      そうならCより魅力的に思える
      2つの変数を常に一緒にロックしなければならない場合をどう扱うのかも知りたい
    • 自分が言語を作るなら、mutable global variableは最初から禁止するだろう
      デバッグしていると、結局いつも問題の根本原因はそこだった
  • Rustの概念密度を指摘した文章について、実際にはそのうち5%だけ知っていても生産的に使えると思う
    12年以上Rustを使ってきたが、#[fundamental] のようなものを使ったことは一度もない
    Rustでもarena allocationはできるし、allocatorという概念も存在する
    デフォルトのallocatorがあるだけで、普通は Box::new のような明示的なヒープ割り当てを使う
    mutable globalは static FOO: Mutex<T> = Mutex::new(...) のように作れ、メモリ安全性のためにmutexが必要になる
    Rustの型システムは、メモリ安全性だけでなくコードの意味的な安全性まで保証するよう設計されている

    • ただし、他の開発者が別の5〜10%の概念を使う可能性があるため、協業では結局さらに多くの概念を学ばなければならない
      Cではこうした複雑さは少ない
      複雑性は結局重要な問題だ
    • 「Rustでもarena allocationが可能だ」というのは事実だが、たいていのRust/Goコードでは小さな単位の多数の割り当てが標準的な経路になっている
      単に可能かどうかではなく、基本的なプログラミングスタイルの違いの話だ
    • Rustでallocatorが型なら、m:nスレッドモデルで各リクエストごとに別個のarenaを与えられるのかも気になる
    • Rustのallocatorがグローバル(global) なのか、すべてのヒープ割り当てが同じallocatorを使うのかも質問している
    • Casey Muratoriのbatch allocation動画に触れつつ、一部の開発者がこれを誤解してRustのRAIIを批判していると指摘している
      Zig Software FoundationがAsahi LinaのRust関連発言を誤って引用した事例もあった
      Zigが他の言語を貶めるマーケティング姿勢はあまり好ましくない
  • Zigが気に入っている理由は、メモリ枯渇を優雅に処理できる言語だからだ
    すべての割り当てが失敗しうる(fallible)ものとして扱われ、明示的に処理しなければならない
    スタック空間も魔法のようには扱わず、コンパイラが呼び出しグラフを解析して最大サイズを推論する
    組み込み環境では、こうした資源中心の設計が不可欠だ

    • ただしLinuxのようにovercommitを使うOSでは、実際には割り当て失敗は発生しない
      言語レベルの処理だけでは解決できない
    • Zigが存在すべき理由は、すでにRustがあるのに何かという問いに対しては、むしろ「CがあるのになぜZigなのか」と聞きたい
      結局は手動メモリ管理という同じ問題を抱えている
      それならGC言語を使うほうがよいと思う
    • 再帰や関数ポインタ呼び出しがあるとき、Zigのスタックサイズ推論がどう動くのか気になる
    • Zigは最初ではなく、1958年のJOVIAL以降のシステム言語の歴史を見る必要がある
    • Rustでもpre-allocationはうまく扱える
      ただしRustの標準ライブラリはOOM時にpanicを使うため、no-std環境で組み込み開発を支える別のエコシステムがある
  • GoのsliceはRustの Vec<T> とは異なる
    append() は新しいsliceを返し、既存メモリを共有することもあればしないこともある
    メモリを減らす方法もなく、append(s, ...) だけ書くと新しいsliceを無視することになる
    Goは「言ったとおりにやれ」という態度で、Rustは「言ったとおりにやったか検証せよ」という態度だ
    つまりGoは単純さのためにミスを許容し、Rustは複雑になってでもミスを減らす方向を選ぶ

    • 実際には slices.Clip でメモリを減らせる
      また append(s, ...) だけではコンパイルエラーになるので、元の文はやや不正確な主張
      Goは機能追加の際に複雑さの増加を慎重に検討する言語だ
    • append(s, …) はそもそもコンパイルすら通らないので、初心者がそうしたミスをすることはないと思う
    • Goにジェネリクスが入ったのに、List[T] のような型が広く使われていないのは興味深い
      おそらくgrowable listを直接受け渡す場面がそれほど多くないからだろう
    • Goでは仕様書やドキュメントにほとんどの落とし穴(foot gun) が明確に書かれている
      単に文書を読まずに驚いているケースが多い
  • C/C++のUB(Undefined Behavior) をランタイム検査で捕まえるのは現実的には難しいと思う
    Androidもすべてのコミットにsanitizerを適用していたが、Rustへ移行してからようやくエクスプロイトが減った

    • Androidのsanitizerに関する主張の出典を求めている
  • 言語比較の記事が各言語の強みと弱みを率直に扱っていてよかった
    ただしRakuに触れられていないのは残念だ
    自分の考えでは、C–Zig–C++–Rust–Goが低レベル言語の連続体だとすれば、高レベル側はJulia–R–Python–Lua–JS–PHP–Raku–WLと続く

    • WLが何を指すのかと尋ねている
    • Rakuは表現力豊かな汎用言語で、多重ディスパッチ・roles・漸進的型付け・遅延評価・強力な正規表現システムを内蔵している
      文法定義を言語レベルで支援するため、DSLやログ解析がしやすい
      VMベースなので性能は低いが、問題の構造をそのまま表現するのに向いている
      Perlの後継として、柔軟で一貫した言語を目指している
  • Rustで関数がポインタを返すとヒープ割り当てが自動で発生すると思うのは誤解だ
    ローカル変数はスタック上にあり、返却時に消えるため、そのポインタは無効になる
    Rustでは安全モードではポインタの逆参照はできず、unsafeモードでは開発者が有効性保証の責任を負う
    おそらく Box::new を「暗黙の割り当て」と勘違いしたのだろう

    • Goのescape analysisとRustの明示的なヒープ割り当てを混同するのは理解しがたい
      これは概念を誤解しているか、意図的にミスリードしているように見える
  • Goの最大の長所は単純な並行性モデル
    goroutineのおかげで並列コードを簡単に書ける

    • Goの利点の1つは、コードの一貫性のおかげで大規模コードベースを探索しやすいことだ
      インターフェース実装を探すのは難しいが、可読性が高くチーム開発に向いている
    • Rob Pikeの“Concurrency is not Parallelism”講演は、Goの並行性哲学をよく説明している
      colored functionがなく、チャネルベースの通信が単純なので、正しい並行コードを素早く書ける
    • ただしStructured Concurrencyのほうがより簡単なモデルだと思う
      関連記事: Structured Concurrency or Go Statement Considered Harmful
    • Zigの新しい std.Io インターフェースはGoの並行性モデルに似ている
      go キーワードは std.Io.async、チャネルは std.Io.Queueselectstd.Io.select に対応する
    • Erlang 開発者なら、Goの並行性モデルが最も簡単だという主張には同意しないだろう
  • 自分が望むのは、Goの単純さにRustの結果/エラー/列挙型処理と、より良いジェネリクスを組み合わせた言語だ

    • 私も同意する。GCがあり、より強力な型システムを持つネイティブ言語には市場需要が大きい
      OCaml、D、Swift、Nim、Crystalなどを見てきたが、まだ市場を支配した言語はない
    • 最新のOCamlは非常に強力な並行性モデルを備えており、性能もGoと競争できると聞く
    • Borgo言語はそうした試みだったが中止された
      代わりにGleamを見てみる価値がある
    • Goのerror proposalは興味深かった
      こうした繰り返し現れる問題を解決できる改善が出ることを期待している
      ジェネリクスは依然として難しい課題だろう
    • 最も近い代替はC#だが、それでもなおOOP中心の言語
  • 文章全体のトーンから、新人開発者の情熱と好奇心が感じられてよかった
    Goのジェネリクス不在は単なるミニマリズムではなく、トレードオフを熟考した結果だったと思う
    Rustのlifetimeは多くの人にとって最大の難関であり、言語の革新性は既存概念の組み合わせにある
    Zigの手動メモリ管理はOOP排除というより、Data-Oriented Design(DOD) の思想に基づいている
    関連講演: AndrewのDOD発表

    • Russ Coxが2009年の“The Generic Dilemma”で示したように、
      「遅いプログラマ、遅いコンパイラ、遅い実行のどれを選ぶか」が核心だった
      Goチームは最終的に、これを満足のいく形で解決する折衷案を見つけたように見える