- 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件のコメント
Hacker Newsの意見
Rustでmutable global variableを作るのは難しくない
ただし
unsafeや同期を提供するスマートポインタを使う必要があるRustは基本的にre-entrantであり、コンパイル時にスレッド安全性を保証するからだ
もし静的なスレッド安全性を気にしないなら、ZigやCのように簡単に作れる
違いは、Rustがコードのランタイム動作についてより多くの保証手段を提供する点にある
他の言語に戻って、こういうものを平然と使っているのを見ると、安全性の面では狂気じみているように感じる
しかし、そうした「単純なこと」が積み重なると、決して単純ではなくなる
Rustはすでにその線を越えていて、もはやtrivialではない
そうならCより魅力的に思える
2つの変数を常に一緒にロックしなければならない場合をどう扱うのかも知りたい
デバッグしていると、結局いつも問題の根本原因はそこだった
Rustの概念密度を指摘した文章について、実際にはそのうち5%だけ知っていても生産的に使えると思う
12年以上Rustを使ってきたが、
#[fundamental]のようなものを使ったことは一度もないRustでもarena allocationはできるし、allocatorという概念も存在する
デフォルトのallocatorがあるだけで、普通は
Box::newのような明示的なヒープ割り当てを使うmutable globalは
static FOO: Mutex<T> = Mutex::new(...)のように作れ、メモリ安全性のためにmutexが必要になるRustの型システムは、メモリ安全性だけでなくコードの意味的な安全性まで保証するよう設計されている
Cではこうした複雑さは少ない
複雑性は結局重要な問題だ
単に可能かどうかではなく、基本的なプログラミングスタイルの違いの話だ
Zig Software FoundationがAsahi LinaのRust関連発言を誤って引用した事例もあった
Zigが他の言語を貶めるマーケティング姿勢はあまり好ましくない
Zigが気に入っている理由は、メモリ枯渇を優雅に処理できる言語だからだ
すべての割り当てが失敗しうる(fallible)ものとして扱われ、明示的に処理しなければならない
スタック空間も魔法のようには扱わず、コンパイラが呼び出しグラフを解析して最大サイズを推論する
組み込み環境では、こうした資源中心の設計が不可欠だ
言語レベルの処理だけでは解決できない
結局は手動メモリ管理という同じ問題を抱えている
それならGC言語を使うほうがよいと思う
ただしRustの標準ライブラリはOOM時にpanicを使うため、no-std環境で組み込み開発を支える別のエコシステムがある
GoのsliceはRustの
Vec<T>とは異なるappend()は新しいsliceを返し、既存メモリを共有することもあればしないこともあるメモリを減らす方法もなく、
append(s, ...)だけ書くと新しいsliceを無視することになるGoは「言ったとおりにやれ」という態度で、Rustは「言ったとおりにやったか検証せよ」という態度だ
つまりGoは単純さのためにミスを許容し、Rustは複雑になってでもミスを減らす方向を選ぶ
また
append(s, ...)だけではコンパイルエラーになるので、元の文はやや不正確な主張だGoは機能追加の際に複雑さの増加を慎重に検討する言語だ
append(s, …)はそもそもコンパイルすら通らないので、初心者がそうしたミスをすることはないと思うおそらくgrowable listを直接受け渡す場面がそれほど多くないからだろう
単に文書を読まずに驚いているケースが多い
C/C++のUB(Undefined Behavior) をランタイム検査で捕まえるのは現実的には難しいと思う
Androidもすべてのコミットにsanitizerを適用していたが、Rustへ移行してからようやくエクスプロイトが減った
言語比較の記事が各言語の強みと弱みを率直に扱っていてよかった
ただしRakuに触れられていないのは残念だ
自分の考えでは、C–Zig–C++–Rust–Goが低レベル言語の連続体だとすれば、高レベル側はJulia–R–Python–Lua–JS–PHP–Raku–WLと続く
文法定義を言語レベルで支援するため、DSLやログ解析がしやすい
VMベースなので性能は低いが、問題の構造をそのまま表現するのに向いている
Perlの後継として、柔軟で一貫した言語を目指している
Rustで関数がポインタを返すとヒープ割り当てが自動で発生すると思うのは誤解だ
ローカル変数はスタック上にあり、返却時に消えるため、そのポインタは無効になる
Rustでは安全モードではポインタの逆参照はできず、unsafeモードでは開発者が有効性保証の責任を負う
おそらく
Box::newを「暗黙の割り当て」と勘違いしたのだろうこれは概念を誤解しているか、意図的にミスリードしているように見える
Goの最大の長所は単純な並行性モデルだ
goroutineのおかげで並列コードを簡単に書ける
インターフェース実装を探すのは難しいが、可読性が高くチーム開発に向いている
colored functionがなく、チャネルベースの通信が単純なので、正しい並行コードを素早く書ける
関連記事: Structured Concurrency or Go Statement Considered Harmful
std.IoインターフェースはGoの並行性モデルに似ているgoキーワードはstd.Io.async、チャネルはstd.Io.Queue、selectはstd.Io.selectに対応する自分が望むのは、Goの単純さにRustの結果/エラー/列挙型処理と、より良いジェネリクスを組み合わせた言語だ
OCaml、D、Swift、Nim、Crystalなどを見てきたが、まだ市場を支配した言語はない
代わりにGleamを見てみる価値がある
こうした繰り返し現れる問題を解決できる改善が出ることを期待している
ジェネリクスは依然として難しい課題だろう
文章全体のトーンから、新人開発者の情熱と好奇心が感じられてよかった
Goのジェネリクス不在は単なるミニマリズムではなく、トレードオフを熟考した結果だったと思う
Rustのlifetimeは多くの人にとって最大の難関であり、言語の革新性は既存概念の組み合わせにある
Zigの手動メモリ管理はOOP排除というより、Data-Oriented Design(DOD) の思想に基づいている
関連講演: AndrewのDOD発表
「遅いプログラマ、遅いコンパイラ、遅い実行のどれを選ぶか」が核心だった
Goチームは最終的に、これを満足のいく形で解決する折衷案を見つけたように見える