/note/tech

Thoughts on Go vs. Rust vs. Zig

要約:

■ 1. はじめに

  • 最近「仕事に適した道具」を使うのではなく仕事にある道具を使っていることに気づいた
  • そのことが自分が知っているプログラミング言語をほぼ決定していた
  • 過去数ヶ月間仕事で使わない言語を実験することに多くの時間を費やした
  • 目標は習熟度ではなく各言語が何に適しているかについて意見を形成することに興味がある
  • プログラミング言語は非常に多くの軸に沿って異なるため比較するのが難しい
  • トレードオフがあるという明らかに真実だが完全に退屈で役に立たない結論にデフォルトでたどり着いてしまう
  • もちろんトレードオフはある
  • 重要な問いはなぜこの言語はこの特定のトレードオフのセットにコミットしたのかである

■ 2. 言語選択の視点

  • この問いは興味深い
  • 加湿器を買うかのように機能のリストに基づいて言語を選びたくない
  • ソフトウェアを構築することと自分のツールを気にかけている
  • 言語はトレードオフを行う際に一連の価値観を表現する
  • どの価値観が自分に共鳴するかを見つけたい
  • この問いは結局のところ機能セットが大幅に重複する言語間の違いを明確にするのにも役立つ
  • 「Go vs. Rust」や「Rust vs. Zig」についてのオンライン質問の数が信頼できる指標であれば人々は混乱している
  • 言語Xは機能a、b、cを持っているのでWebサービスを書くのに優れているが言語Yは機能aとbしか持っていないということを覚えるのは難しい
  • 言語Xは言語Yがインターネットを嫌いインターネット全体のプラグを抜くべきだと信じている人によって設計されたためWebサービスを書くのに優れていると覚える方が簡単だと思う

■ 3. 本文の目的

  • 最近実験した3つの言語についての印象を収集した
  • Go、Rust、Zigである
  • 各言語での経験をその言語が何を重視しそれらの価値をどれだけうまく実行するかについての包括的な評決に統合しようと試みた
  • これは還元的かもしれないが還元的な偏見のセットを結晶化することがここでやろうとしていることである

■ 4. Goの特徴

  • Goはミニマリズムによって特徴づけられる
  • 「現代のC」と表現されている
  • GoはCのようではないがガベージコレクションされ実際のランタイムを持っている
  • 言語全体を頭の中に収めることができるという点でCに似ている
  • 言語全体を頭の中に収めることができるのはGoが非常に少ない機能しか持っていないからである
  • 長い間Goはジェネリクスを持っていないことで悪名高かった
  • これは最終的にGo 1.18で変更されたがそれは12年間人々がジェネリクスを言語に追加するよう懇願した後のことであった
  • タグ付きユニオンやエラー処理の糖衣構文など現代の言語で一般的な他の機能はGoに追加されていない
  • Go開発チームは言語に機能を追加するためのハードルが高いようである
  • 最終結果は別の言語でより簡潔に表現できるロジックを実装するために多くのボイラープレートコードを書くことを強いられる言語である
  • しかし結果は時間の経過とともに安定しており読みやすい言語でもある

■ 5. Goのスライス型の例

  • Goのミニマリズムの別の例としてGoのスライス型を考える
  • RustとZigの両方にスライス型があるがこれらはファットポインタでありファットポインタのみである
  • Goではスライスはメモリ内の連続したシーケンスへのファットポインタであるがスライスは成長することもできる
  • つまりRustのVec型とZigのArrayListの機能を包含している
  • またGoはメモリを管理してくれるためGoはスライスのバッキングメモリがスタックとヒープのどちらに存在するかを決定する
  • RustやZigではメモリがどこに存在するかについてはるかに真剣に考える必要がある

■ 6. Goの起源と目的

  • Goの起源神話は基本的に次のようなものである
  • Rob PikeはC++プロジェクトのコンパイルを待つことにうんざりしていた
  • GoogleのC++プロジェクトで他のプログラマーがミスを犯すことにもうんざりしていた
  • したがってGoはC++がバロック的であるところでシンプルである
  • それはプログラミングの一般階級のための言語である
  • 90%のユースケースに十分であると同時に理解しやすいように設計されている
  • 特に並行コードを書く際にそうである
  • 仕事でGoを使っていないが使うべきだと思う
  • Goは企業のコラボレーションに奉仕するためにミニマルである
  • これを軽蔑として意味しているわけではない
  • 企業環境でソフトウェアを構築することには独自の課題がありGoはそれを解決する

■ 7. Rustの特徴

  • Goがミニマリストであるのに対しRustはマキシマリストである
  • Rustに関連付けられることが多いタグラインは「ゼロコスト抽象化」である
  • これを「ゼロコスト抽象化、そしてたくさんの抽象化!」と修正したい
  • Rustは学習が難しいという評判がある
  • Rustを難しくしているのはライフタイムではなく言語に詰め込まれた概念の数であるというJamie Brandonの意見に同意する
  • 特定のGithubコメントを取り上げるのは最初の人ではないがそれはRustの概念密度を完璧に示している
  • もちろんRustはGoがミニマリストであろうとするのと同じ方法でマキシマリストであろうとしているわけではない
  • Rustが複雑な言語であるのはそれが達成しようとしていることが安全性とパフォーマンスという幾分緊張関係にある2つの目標を実現することだからである

■ 8. Rustの安全性とパフォーマンス

  • パフォーマンス目標は自明である
  • 「安全性」が何を意味するかはそれほど明確ではない
  • 少なくとも自分にとってはそうだったがおそらく長い間Pythonに慣れすぎていたのかもしれない
  • 「安全性」は「メモリ安全性」を意味する
  • 無効なポインタを逆参照したりダブルフリーを行ったりできないようにするという考えである
  • しかしそれはそれ以上のことを意味する
  • 「安全な」プログラムはすべての未定義動作(UBと呼ばれることもある)を回避する
  • 恐ろしいUBとは何か
  • それを理解する最良の方法は実行中のプログラムには死よりも悪い運命があることを覚えておくことだと思う
  • プログラムで何か問題が発生した場合即座の終了は実際には素晴らしい
  • なぜならエラーがキャッチされない場合の代替案はプログラムが予測不可能性の薄明の領域に入り込むことだからである
  • そこでは次のデータ競合に勝つスレッドや特定のメモリアドレスにたまたまあるガベージによって動作が決定されるかもしれない
  • 今やハイゼンバグとセキュリティ脆弱性がある
  • 非常に悪い

■ 9. Rustのコンパイル時チェック

  • Rustはコンパイル時にチェックすることによって実行時のパフォーマンスペナルティを支払うことなくUBを防ごうとする
  • Rustコンパイラは賢いが全知ではない
  • コンパイラがコードをチェックできるようにするにはコードが実行時に何をするかを理解する必要がある
  • したがってRustには表現力豊かな型システムと特性のメナジェリーがあり別の言語では単に明白な実行時の動作であるものをコンパイラに表現できる
  • これによりRustは難しくなる
  • なぜならただ物事を行うことができないからである
  • Rustがその物事に対して持っている名前を見つけ出す必要がある
  • 必要な特性などを見つけてRustが期待するように実装する必要がある
  • しかしこれを行えばRustは他の言語ができないコードの動作について保証を行うことができる
  • これはアプリケーションによっては重要かもしれない
  • また他の人のコードについても保証を行うことができるためRustではライブラリを消費することが簡単になる
  • これがRustプロジェクトがJavaScriptエコシステムのプロジェクトとほぼ同じくらい多くの依存関係を持つ理由を説明している

■ 10. Zigの特徴

  • 3つの言語のうちZigは最も新しく最も成熟していない
  • この執筆時点でZigはバージョン0.14にすぎない
  • その標準ライブラリにはほとんどドキュメントがない
  • それを使用する方法を学ぶ最良の方法はソースコードを直接参照することである
  • これが真実かどうかはわからないがZigをGoとRustの両方への反応として考えたい
  • Goはコンピュータが実際にどのように機能するかについての詳細を曖昧にするためシンプルである
  • Rustはその多くのフープをくぐらせることを強制するため安全である
  • Zigはあなたを自由にする
  • Zigではあなたが宇宙を制御し誰もあなたに何をすべきか言うことはできない

■ 11. Zigのメモリ管理

  • GoとRustの両方でヒープ上にオブジェクトを割り当てることは関数から構造体へのポインタを返すのと同じくらい簡単である
  • 割り当ては暗黙的である
  • Zigでは自分で明示的にすべてのバイトを割り当てる
  • Zigは手動メモリ管理を持つ
  • Cよりもさらに多くの制御がある
  • バイトを割り当てるには特定の種類のアロケータでalloc()を呼び出す必要がある
  • つまりユースケースに最適なアロケータ実装を決定する必要がある
  • Rustでは可変グローバル変数を作成することが非常に難しくそれを行う方法についての長いフォーラムディスカッションがある
  • Zigでは問題なく作成できる

■ 12. Zigの不正動作の扱い

  • 未定義動作はZigでも依然として重要である
  • Zigはそれを「不正動作」と呼ぶ
  • 実行時にそれを検出し発生時にプログラムをクラッシュさせようとする
  • これらのチェックのパフォーマンスコストを心配する人のためにZigはプログラムをビルドする際に選択できる4つの異なる「リリースモード」を提供する
  • これらのいくつかではチェックが無効になっている
  • アイデアはチェックされたリリースモードでプログラムを十分な回数実行してチェックされていないビルドで不正動作がないことについて合理的な確信を持つことができるようにすることのようである
  • これは非常に実用的な設計のように思える

■ 13. Zigとオブジェクト指向プログラミング

  • ZigとGoとRustの別の違いはZigのオブジェクト指向プログラミングとの関係である
  • OOPはしばらくの間不人気でありGoとRustの両方がクラス継承を避けている
  • しかしGoとRustは他のオブジェクト指向プログラミングイディオムに対して十分なサポートを持っているため望むならプログラムを相互作用するオブジェクトのグラフとして構築することができる
  • Zigにはメソッドがあるがプライベート構造体フィールドや実行時ポリモーフィズム(動的ディスパッチとも呼ばれる)を実装する言語機能はない
  • std.mem.Allocatorはインターフェースになりたがっているにもかかわらずである
  • 私が知る限りこれらの除外は意図的である
  • Zigはデータ指向設計のための言語である

■ 14. Zigの手動メモリ管理の意義

  • 2025年に手動メモリ管理を使用してプログラミング言語を構築することは狂気に思えるかもしれない
  • 特にRustがガベージコレクションさえ必要とせずコンパイラにそれを行わせることができることを示した時には
  • しかしこれはOOP機能を除外する選択と非常に関連した設計選択である
  • GoやRustやその他の多くの言語ではオブジェクトグラフ内の各オブジェクトに対して一度に少しずつメモリを割り当てる傾向がある
  • プログラムには何千もの小さな隠されたmalloc()とfree()がありしたがって何千もの異なるライフタイムがある
  • これがRAIIである
  • Zigでは手動メモリ管理が多くの面倒でエラーが発生しやすい簿記を必要とするように思えるかもしれないがそれはすべての小さなオブジェクトにメモリ割り当てを結びつけることにこだわる場合のみである
  • 代わりにプログラム内の特定の賢明なポイント(イベントループの各反復の開始時など)で大きなメモリのチャンクを割り当てて解放し操作する必要があるデータを保持するためにそのメモリを使用することもできる
  • Zigが奨励しているのはこのアプローチである

■ 15. ZigとRustの違い

  • 多くの人々がRustが既に存在するのになぜZigが存在すべきなのかについて混乱しているようである
  • Zigがよりシンプルであろうとしているだけではない
  • この違いがより重要なものだと思う
  • Zigはコードからさらにオブジェクト指向的な思考を切除することを望んでいる
  • Zigには楽しく破壊的な雰囲気がある
  • それは(オブジェクトの)企業のクラス階層を打ち砕くための言語である
  • それは誇大妄想狂とアナーキストのための言語である
  • 好きである
  • すぐに安定版リリースに到達することを望むがZigチームの現在の優先事項はすべての依存関係を書き直すことのようである
  • Zig 1.0を見る前にLinuxカーネルを書き直そうとすることは不可能ではない