※本記事は、Zigプログラミング言語の創始者およびZig Software FoundationのファウンダーとプレジデントであるAndrew Kelley氏、SourcegraphのCo-founderおよびCTOであるBeyang Liu氏、SourcegraphのソフトウェアエンジニアであるStephen Gutekanst氏が出演するSourcegraph Podcastのエピソード「S2, E7: Taking the warts off C, with Andrew Kelley, creator of the Zig Software Foundation」の内容を基に作成されています。動画およびショーノートの詳細は https://www.youtube.com/watch?v=gn3YsZ6HUHw でご覧いただけます。本記事では、動画の内容を要約しております。なお、本記事の内容は原著作者の見解を正確に反映するよう努めていますが、要約や解釈による誤りがある可能性もありますので、正確な情報や文脈については、オリジナルの動画をご視聴いただくことをお勧めいたします。
1. イントロダクション——番組紹介とゲスト、プログラミングとの出会い
1-1. 番組紹介とゲスト紹介
Beyang: Sourcecraftポッドキャストへようこそ。数ヶ月前、チームメイトのStephenがZigというものについてツイートしているのに気づきました。最初はあまり気に留めていなかったのですが、Stephenは非常に優秀なプログラマーで、Sourcegraphのコアな機能をスタック全体にわたって構築してきた人物です。検索バックエンドやインデックス技術のハッキングから、自宅の猫たちのためにミニチュアの城を作るといった多彩なサイドプロジェクトまで幅広く取り組んでいます。そのStephenがZigについて継続的にツイートし、Zigで構築しているクールなものを発信し続けるのを見て、改めて詳しく調べてみることにしました。調べてみると、自分にとって新鮮なとても面白いプログラミング言語と、その周囲に育ちつつある素晴らしいコミュニティを発見しました。ZigはCの置き換えを目指すシステムプログラミング言語です。本日はStephenとともに、Zigプログラミング言語の生みの親であり、Zig Software FoundationのファウンダーおよびプレジデントであるAndrew Kelleyさんをゲストにお迎えしています。Andrewさん、今日はありがとうございます。
Andrew: お招きいただきありがとうございます。
Stephen: 参加できて光栄です。
1-2. AndrewとStephenのプログラミングとの出会い
Beyang: まず、Andrewさんがそもそもプログラミングを始めたきっかけはどのようなものだったのでしょうか。
Andrew: 割とよくある話だと思いますが、子供の頃からビデオゲームが好きで、特に自分でレベルを作れるゲームに夢中になっていました。プログラミングを知ったとき、これこそが究極のゲームだと感じました。自分でゲームを作れるわけですから。コンピュータを思い通りに動かせるという感覚の魔力に引き込まれたのだと思います。12歳のとき、最初に触れたのは「Liberty Basic」というCDが付属する入門書でした。誰も知らないような言語でしたが、Basic系の言語でプログラミングを教えるための個人プロジェクトとして作られたものでした。最初に作ったプログラムは、「名前は?」「北に進む?東に進む?」といった選択肢を提示するダイアログツリー形式のゲームのようなものでした。
Beyang: Stephenさんもビデオゲームを通じてプログラミングに入ったんじゃないでしたっけ?
Stephen: そうですね。一番最初はCSSでゲームを作ろうとしていたと思います。当時のCSSはそういった用途には向いていませんでしたが(笑)。プログラミング言語的な意味では「関数的」だったかもしれません。ゲームロジックとしては全然機能していませんでしたが。
Andrew: Liberty Basicの後はVisual Basic 6に移りました。プロジェクトを開くと新しいフォームがすぐに表示されて、すぐに実行できる。何もないフォームに「Form1」と書かれているだけなのですが、それがまるで招待状のように感じられて、何かをドラッグして置いて、すぐに何かを作り始められる。WYSIWYGのウェブエディタを作りたい、ゲームを作りたい、マウスを自動で動かすマクロを作りたい——思いついたことを何でも試していました。思いついたことを何でもコンピュータに実現させられる魔法に完全に魅了されていて、小さな実験プログラムを詰め込んだフォルダがどんどん膨らんでいきました。
2. Zigプロジェクトの誕生と歩み——着想からフルタイム開発へ
2-1. DAW開発中に芽生えた動機と最初のコミット(2015年8月)
Beyang: AndrewさんはOkCupidにエンジニアとして在籍されていましたが、Zigのアイデアはその頃に生まれたのでしょうか。
Andrew: 実はOkCupidに入る時点で、すでに3年間Zigに取り組んでいました。Zigプロジェクトはキャリアのセメントの隙間から咲く花、と表現したことがあります。私はコンピュータのあらゆるトピックに魅了されていて、副業として純粋に学ぶ楽しさのためにリサーチプロジェクトを追いかけるのが常でした。キャリアのパターンはずっと同じで、お金を貯めて、仕事を辞めて、サイドプロジェクトで遊んで、お金が尽きたら就職する、というサイクルの繰り返しです。Zigを始めたのは今からおよそ6年前で、OkCupidに行く前に1年間無職でフルタイムでZigに取り組んでいました。
Beyang: そもそもZigを始めようと思った具体的なきっかけは何だったのでしょうか。
Andrew: その年は、小さなプロジェクトをとっかえひっかえするのではなく、一つのプロジェクトに腰を据えて取り組む経験を積みたいと思っていました。そこで取り組んでいたのがデジタル音楽制作ソフト(DAW)の開発です。今もそのプロジェクトは持っていますが、現在は棚上げ状態です。いつか再開したいと思っています。そのDAWを開発している最中、頭の片隅でずっとこんな考えが引っかかっていました。「このソフトを書くのにC++は適切なツールではない。もっと良い言語が必要だ」と。それがZigを作り始めた直接のきっかけです。最初のコミットは2015年8月5日で、gitのログを遡って確認しました。
2-2. 仕事と副業としての開発期間——貯金・離職・復職のサイクル
Beyang: 2015年から始まって、OkCupidやその間にあったBacetraceという別のスタートアップでも働きながら、ずっとZigを夜や週末に続けていたわけですね。
Andrew: そうです。それはかなりきつかったですね。フルタイムの仕事だけでも消耗しますし、そこに夜や週末をプログラミング言語の開発に充てるというのは、健康面にも社交面にも代償を払うことになります。本当に大きな夢を二つ同時に抱えているようなものでした。
Beyang: 同じような状況にいる人たちへのアドバイスはありますか。副業として何かを追いかけながら、本業も手放せないという状況です。
Andrew: 後悔はしていません。人それぞれ自分の人生の意味をどこに見出すかは違いますから。ただ、もう少し健全にやる方法として伝えたいのは、趣味に資金を得ることは思っているより簡単かもしれないということです。プログラマーという職業は恵まれていて、まず貯蓄を比較的早く積める点が挙げられます。それだけでなく、少人数の開発者が作ったソフトウェアで非常に多くのユーザーが恩恵を受けられる、という構造があります。これはビッグOの計算量と同じような話で、少ない労力で多くの人が恩恵を受けるなら、それぞれが少しずつお金を出してくれるだけで成立します。寄付収入はそれほど大きくはありませんが、支払いが必要な人数も少ないので、数式として成り立つケースが実は多いのです。少し創意工夫すれば、仕事なしでやっていける可能性があります。
2-3. 寄付による収入の漸増とフルタイム移行の判断、Zig Software Foundationの設立
Beyang: 副業として続けていたZigが、フルタイムで取り組める段階になったのはいつ頃のことでしょうか。どのように判断されたのですか。
Andrew: 最初はPatreonを通じた個人への直接寄付だけでした。Patreonは10%を手数料として取っていくので、正直どうかと思いましたが、当時はそれが唯一の選択肢でした。Zig Software Foundationが設立されたのはずっと後の話です。寄付の推移を見ていると、ブログ記事を書いたときや、詳細なリリースノート付きのリリースを出したときに伸びる傾向があって、全体的には右肩上がりで推移していました。OkCupidで働いていたとき、手元の貯蓄額、毎月の寄付収入、その増加ペースを保守的に見積もって計算してみました。すると、今すぐ辞めれば貯蓄は一時的に減るものの、ゼロに達する前に寄付収入が上回る水準に達する、という見通しが立ったのです。その判断を下したのが今から約2年前のことです。ブログ記事に正確な日付が残っています。プロジェクト全体を通じて、寄付額も貢献者のプルリクエストやIssueの数も、急激に爆発的に伸びた瞬間はなく、すべてが非常に緩やかな成長曲線をたどってきました。
3. 言語設計の哲学——実験と失敗を繰り返すインクリメンタルな進化
3-1. オープンソースによる公開開発と「骨格を先に見せる」貢献者獲得戦略
Beyang: 2015年の最初のコミットから現在まで、言語の技術的な進化はどのように進んできたのでしょうか。コンパイル時コード実行、メモリアロケーション、async/awaitといった機能は、一度に追加されたわけではないと思いますが。
Andrew: おっしゃる通り、すべてインクリメンタルに進んできました。最初からオープンソースで開発していて、大きな未マージのブランチを抱えたまま長期間作業するようなことはほとんどありませんでした。コードはできるだけ早く使える状態にするのが好きなので、放置されたまま腐っていくコードを作りたくないんです。プロジェクトに対して持っていたビジョン自体は最初から一貫していましたが、注意・モチベーション・エネルギーはプロジェクトのさまざまな側面の間を行き来していました。それは今振り返っても正しい判断だったと思っています。モチベーションとエネルギーをうまく管理することは、長期プロジェクトを成功させるための根幹となるメタスキルだと感じています。
さらに、この「あちこち飛び回る」アプローチには貢献者獲得という副次的な効果もありました。プロジェクトの骨格を早めに見せることで、ビジョンを形として示せるからです。骨格を見た人が「ああ、こういうものを作ろうとしているんだな、ここに肉付けできる」と感じて貢献してくれる。自分のモチベーションを新鮮に保てる上に、ビジョンを見た人が足りない部分を埋めてくれる、という相乗効果が生まれました。
3-2. 仮説・実験・観察の繰り返し——「すべての直感は間違っていた」という気づき
Beyang: 言語設計の決定はどの程度が事前に設計・推論できたもので、どの程度が実際に試してみないと分からないものでしたか。
Andrew: ほぼすべてがイテラティブなプロセスでした。言語設計上のほぼあらゆる決定は最初の状態のままではありません。自分の直感はことごとく間違っていましたし、実装もことごとく間違っていた。プロジェクト全体が、謙虚さを学び続け、何がうまくいっていないかに注意を払い、再考して、別のものを試すという繰り返しでした。プロセスとしては「仮説を立てて実装し、自分と他の人の体験を観察する」という形です。ペースは速く、混沌としていて、実際に言語を使おうとする人にとっては破壊的な変更も多い。しかしその分、慎重なアプローチでは得られない実際のフィードバックをもとに野心的なアイデアを試せるという強みがあります。
3-3. async/await設計の失敗と学習——LLVMコルーチンを試みた第1版の教訓
Andrew: 事前にもう少し設計が必要だったケースを一つ挙げるとすれば、Zigのasync/await機能です。最初に試みたのはLLVMのコルーチンサポートをベースにするアプローチでした。アイデアが浮かんだらすぐに実装して試す、というのはいつも通りです。ただ、LLVMコルーチンベースの第1版には非常に多くの問題がありました。しかしこの失敗した第1版を作ったことで、問題領域を深く理解できました。その経験があったからこそ、2回目の設計イテレーションでははるかに的を射たものになりました。最初の試みがなぜうまくいかなかったのかを実体験として理解していたので、次の設計がずっとよいものになったのです。
3-4. Zigの禅(Zig Zen)——「ローカル最大値を避けよ」という設計原則
Beyang: 個々の「紙やすり傷(paper cut)」を一つひとつ修正するというアプローチが、アロケータ渡しのような、もっと大きな設計変更にまで発展していくのはなぜでしょうか。もっと大きな設計原則のようなものがあるのでしょうか。
Andrew: そうですね、Pythonの「Pythonの禅」に着想を得て、「Zigの禅(Zig Zen)」を作りました。その中で私が最も気に入っている原則が「ローカル最大値を避けよ(avoid local maximums)」です。数学のグラフで言えば、グローバルな最大値が本当に目指すべき場所ですが、手近なところにローカルな最大値があって、そこが一番良い場所のように見えてしまうことがある。そのローカル最大値にいると、グローバルな最大値に向かうためにはいったん下り坂を進まなければならない。それが私たちのミッションです。「まあこれで十分」という妥協を良しとせず、より良い形に組み直すためなら既存のものを壊すことも辞さない、という姿勢です。
Beyang: それは言語設計そのものに対してですか、それともZigで実装されるプログラムに対しての指針でもあるのでしょうか。
Andrew: 両方に対する指針です。言語設計においては、ある言語機能を作ったけれど問題があると分かったら、全ユーザーのコードを壊してでもバージョン2を試みる、という意思決定に表れています。1.0をタグ付けするまでの間に、仮定を継続的に再評価するためにこの時間を使い続けます。「まあこれでいいか」という安易な妥協をせず、いったん下り坂を歩いてでもより良い解に到達することを厭わない——それがZigという言語そのものの設計姿勢であり、Zigで書くプログラムに対しても同じ姿勢を推奨しています。
4. CのイボをとるZigの言語機能——プリプロセッサ・ジェネリクス・コンパイル時実行
4-1. プリプロセッサを不要にする——定数とコンパイル時コード実行による解決
Beyang: ZigはCに馴染みのある人ならすぐに習得できると言われていますが、具体的にどのような違いがあるのでしょうか。
Andrew: Cを知っているなら、Zigはすでに知っているも同然です。言語リファレンスを読んでいけば、概念を一対一で対応させながらさらっと読み進められると思います。違いはどちらかというと哲学的なもの、つまり特定の問題をどう解決するかという細部の違いです。Cのプログラマーがまず戸惑うのは、プリプロセッサを使おうとしたら存在しないことに気づく点です。その代わりに、コンパイル時にコードを実行するという別のツールがあります。
Beyang: 少し問答形式で確認させてください。Cでは#defineをよく使いますが、なぜ定数ではなく#defineを使うのでしょうか。
Andrew: なぜだと思いますか?
Beyang: うーん、他のコードがそうしていたから、というのが正直なところで、あまり深く考えていませんでした。効率の問題で、#defineはコンパイル後に取り除かれるから、定数だと直接バイナリに埋め込まれるから……でしょうか。
Andrew: それは定数でも同じことが起きるので、その理由ではないですね。正解はこうです。定数を使おうとして、たとえば配列の長さを指定する場面で使おうとすると、コンパイルエラーになるからです。Cの配列はコンパイル時に長さが決まっていなければなりませんが、コンパイラがその定数からその事実を推論するほど賢くないのです。定数なのに、なぜコンパイル時に分からないのかと思いますよね。Zigはそこをただ修正しました。それだけの話です。
4-2. 条件付きコンパイルの実現——#ifdefに代わるコンパイル時if文
Beyang: #ifdefを使った条件付きコンパイルはどうでしょうか。WindowsとPOSIXで異なるコードをビルドするような場面です。
Andrew: Zigではそれはただのif文です。重要なのは、if文の条件がコンパイル時に既知かどうかをZigが判定する点です。コンパイル時に既知であれば、デッドブランチは評価されません。つまり、デッドブランチの中にコンパイルエラーを置いても、そのブランチが死んでいる限りエラーにはなりません。これが条件付きコンパイルの実現方法です。Cでプリプロセッサが必要だった理由は、本質的にこの2つだけでした。
Beyang: なるほど、シンプルですね。C++にはテンプレートによるジェネリクスもありますが、Zigにはどのように対応するものがあるのでしょうか。
Andrew: C++のテンプレートも同じ考え方の延長線上にあります。ZigにはすでにコンパイルにコンパイルTime既知という概念がありました。あとは、それを関数のパラメータに対しても適用できるようにしただけです。
4-3. ジェネリクスの自然な導出——コンパイル時既知パラメータとしての型
Andrew: つまり、Zigのジェネリクスとは、1つ以上のパラメータがコンパイル時に既知でなければならないと宣言された関数のことです。そのコンパイル時既知のパラメータとして、たとえばi32のような型を渡せます。ジェネリックなデータ構造が欲しければ、型をパラメータとして受け取る関数を書くだけです。わざわざ別の概念を導入する必要はありませんでした。
Stephen: これは私が最初にZigに触れたとき、特に印象的だった点です。コンパイル時実行の概念を学んで「なるほど、面白いけどどう感じるかはまだわからない」と思っていたところ、ジェネリクスを実現するときに「ああ、これはさっき学んだことの自然な拡張に過ぎないんだ」と気づきました。まったく同じ書き方で、型をパラメータとして渡すだけです。Goでジェネリクスの議論が何年も続いていたのとは対照的に、Zigではジェネリクスがコンパイル時実行から自然に導出されました。他の言語ではジェネリクスが後から積み重ねられた別のシステムになっていることが多く、C++のプリプロセッサとテンプレートの学習体験がそれぞれ切り離されているようなものだと思います。Zigではその学習体験が連続していて、非常に自然な習得曲線でした。
Andrew: まさにその通りです。Cの紙やすり傷を一つひとつ修正していったら、ジェネリクスが手元に転がり込んできた、という感じです。
4-4. 隠れた制御フロー・暗黙のメモリ確保・マクロの排除というハードラインな哲学
Beyang: 隠れた制御フローなし、隠れたメモリアロケーションなし、プリプロセッサなし、マクロなし、というZigの方針は、途中で摩擦を生むことはありましたか。他の言語にはあるものをあえて取り除いているわけですから。
Andrew: この哲学は最初から一貫したハードラインのスタンスです。実はこれはZigのホームページに書いてあることであり、Zigプログラマーになるかならないかを振り分けるソーティングハットのような役割を果たしています。それを読んで「いい、これが欲しかった」と思う人がZigプログラマーになり、「意味が分からない、そういう機能が欲しい」と思う人はZigプログラマーにならない。それでいいのです。
Stephen: Goのコミュニティにも通じるエトスですよね。暗黙より明示、魔法のようなことはしない、という姿勢です。Zigも同じエネルギーを持っていると感じます。
Andrew: まったく同感です。「no implicit control flow(暗黙の制御フローなし)」というZigのサイトの記述を読んだとき、それがZigプログラマーかどうかを分ける分岐点になります。機能の少なさを良しとするか、もっと機能が豊富な言語を選ぶか——それぞれが自分の哲学に合った選択をすればいいのです。
5. グローバルアロケータの廃止——アロケータ渡し設計がもたらした想定外の恩恵
5-1. アロケータを引数として渡す設計思想とその背景
Beyang: Zigにはグローバルアロケータがないということですが、改めて整理させてください。メモリが欲しければアロケータをパラメータとして渡す、つまりグローバルなmalloc関数のようなものは存在しない、ということでしょうか。異なるアロケータには異なるメモリ管理アルゴリズムが実装されている、という理解で合っていますか。
Andrew: そのとおりです。そしてこの設計がもたらすメリットについては、実装した当初は自分でもすべて想定していたわけではありませんでした。やっていくうちに、次々と新しい恩恵を発見し続けているという状況です。
Beyang: 根本的な疑問として、なぜCにはmallocという全員が使わざるを得ないグローバルな関数が一つしかないのでしょうか。自分でもずっと当然のものとして受け入れてきましたが、改めて考えると不思議ですね。
Andrew: まさにそこがZigプロジェクトの大きなミッションの一つです。私たちが当然のものとして受け入れてきた抽象化のレイヤーを剥がして、その根本的な前提を問い直し、もしかしたら調整や改善、あるいは完全なやり直しができるのではないかと考えることです。mallocが特別なものであるという思い込みを疑ってみると、実はそうである必要はなかったということが分かります。
5-2. カーネル・組み込み・WebAssemblyでの標準ライブラリ再利用
Beyang: 具体的にどのような恩恵があるのか、もう少し詳しく教えてください。
Andrew: 分かりやすい例として、ハッシュマップのデータ構造を考えてみましょう。Zigの標準ライブラリには実際に非常に良いハッシュマップの実装があります。キーの順序を保持するArrayHashMapと、順序が不要な場合に使えるSwiss Tables方式の実装があり、APIの設計には自分でも満足しています。CやC++でこのようなデータ構造を実装すると、内部でmallocを呼び出すことになります。するとこのライブラリをカーネルの中や、Arduinoなどの組み込みデバイスや、WebAssemblyの環境で使おうとすると使えなくなります。mallocが呼び出せない環境だからです。あるいは呼び出せるとしても、奇妙なシム(shim)コードを追加しなければなりません。
Zigではアロケータを引数として渡すので、デスクトップアプリケーションならmallocベースのアロケータを渡せばいいし、カーネルを書いているならカーネル用のアロケータを渡せばいい。ハッシュマップのデータ構造を書き直す必要はなく、標準ライブラリのものをそのまま使えるのです。あらゆるユースケースに対して同じコードが再利用できる。これがアロケータ渡し設計の核心的な利点です。
Beyang: なるほど、これまでCのライブラリをこういった環境に持ち込もうとすると標準ライブラリが使えなくて困った、というのは自分も経験があります。たとえばstring.hをインクルードしようとして謎のコンパイルエラーが出る、あの体験ですね。
Andrew: そうです。カーネルを作っているときにメモリ上で文字列を操作したいだけなのに、string.hが見つからないというエラーが出る。なぜ?という疑問から始まって、その前提を問い直すと、実はそうである必要はなかったという結論に至る。Zigの設計はそういった問い直しの積み重ねでできています。
5-3. テスト時のメモリリーク自動検出——テスト用グローバルアロケータの活用
Andrew: アロケータ渡し設計がもたらした想定外の恩恵がもう一つあります。Zigにはユニットテスト用に推奨されるグローバルアロケータが存在していて、このアロケータはメモリリークがあるとユニットテストを失敗させます。つまりzig testを実行するだけで、メインアプリケーションとして実際に動かす前に、メモリリークをすぐに発見できるのです。
Beyang: それは言われてみれば当然のことのようにも聞こえますが、これまでずっとmallocが特別なものだという思い込みがあったので、気づけませんでしたね。
Andrew: まさにその通りです。アロケータを一つに固定するのではなく、用途ごとに差し替えられる設計にしただけで、テスト環境でのメモリリーク検出という全く別の恩恵が転がり込んできました。実装当初は想定していなかった利点が次々と見えてくる、というのがこの設計の面白いところです。
Stephen: アロケータを差し替えられることには、メモリ効率やパフォーマンスの観点からも大きな意味があると思います。たとえばデータ構造ごとに異なるアロケータ戦略を試せるので、グローバルなアロケータを別のものに切り替えるようなリンク作業なしに、データ構造単位で最適なメモリ管理を実験できます。昨夜、ZigのUnicodeパッケージであるzigglyphのコードを見ていたのですが、作者が汎用アロケータをアリーナアロケータに交換したところ、全ベンチマークで約2倍の速度向上が得られたというケースを目にしました。データ構造単位でアロケータを使い分けられるという設計が、これほど大きな最適化の余地を生み出すというのは非常に興味深いと思います。
Andrew: それは素晴らしい例ですね。こういった最適化のパターンは、他の言語でも技術的には可能ではあるのですが、Zigではそれが自然な慣習として組み込まれているため、開発者が意識的に取り組みやすい環境になっています。
6. 標準ライブラリの現状と「ジェネラルパーパス」という野心的な目標
6-1. 標準ライブラリは言語の実験台——整合性より必要に応じた追加という現状
Beyang: 標準ライブラリはどのように進化してきたのでしょうか。GoのI/Oパッケージの構造に着想を得ているように見える部分もありますが、一方でHTTPのような機能は省かれているようです。標準ライブラリの設計にどのような哲学があるのか、また今後どういう方向に向かうのかを教えてください。
Andrew: 正直に言うと、現状では標準ライブラリの一部は非常に堅牢ですが、一部は完全に混乱した状態です。標準ライブラリの主な目的は、最初からずっと、そして今でも大部分において、言語そのものの実験台として機能することです。言語が安定してから初めて、標準ライブラリを改めて批判的な目で見直す段階に入ると思っています。「プログラミング言語の標準ライブラリはどうあるべきか」という問いに向き合って、きちんと監査する作業はまだできていません。
現状の標準ライブラリは、私が何かを作っているときに必要だったもの、他の貢献者が何かを作っているときに必要だったもの、あとはプルリクエストを送ってきてくれた人が必要としていたもの——それらが積み重なったものに過ぎません。キュレーションの観点からはまさに混乱した状態です。ただ「混乱している」と言っても、コードの品質自体は実際に高いものが多く、あくまで何があって何がないかという点での話です。
6-2. HTTPクライアント・サーバーの位置付けとパッケージマネージャとの関係
Beyang: ではウェブサーバー向けのHTTPクライアントやサーバーは、標準ライブラリに入る見込みはあるのでしょうか。
Andrew: まだ判断していません。おそらくパッケージマネージャに必要なものは標準ライブラリに入ることになると思いますが、パッケージマネージャにはクライアントが必要になるはずです。クライアントがあればテストのためにサーバーも必要になるかもしれない。そういう流れでHTTPサポートが入る可能性はあります。ただし、それが確定したわけではなく、今後の判断次第です。
Beyang: Goの標準ライブラリが非常に充実しているのは、開発者たちがGoogleのサーバーサイド開発のニーズを念頭に置いていたからだという見方があります。Zigには、標準ライブラリの充実度を決める際に念頭に置いている「ターゲットとなるプログラム」のようなものはありますか。
Andrew: Goを見ると、意図したユースケースが言語ににじみ出ているというのはよく分かります。Goはほぼ公式に、サーバー開発のために作られたと言ってもいいでしょう。Zigのタグラインは「ジェネラルパーパス・プログラミング言語およびツールチェーン」です。そして多くの新しい言語において、セルフホストコンパイラが最初のターゲットプログラムになることが多いですが、Zigも例外ではありません。
6-3. 「ジェネラルパーパス」タグラインの意図——あらゆるユースケースへの対応
Andrew: 「ジェネラルパーパス」というのは非常に意図的で野心的な宣言です。「本当に何にでも使える」という意味ですから、非常に広いスコープを持つ大きな主張です。しかし私たちはこの主張を真剣に受け止めています。どんな問題領域を例に挙げてもらっても、そのユースケースに対してZigがどう対応するかを示せます。あらゆるユースケースにおいて、ハードウェアを最適に活用できる方法を提供することを意図しており、しかもそのコードが再利用可能であることを目指しています。たとえばハッシュマップのような実装であれば、カーネルでもデスクトップアプリケーションでも同じコードが使える——それがZigの「ジェネラルパーパス」という言葉の意味です。
Beyang: 実際にそれが実現できているという確信はあるのでしょうか。かなり大きな主張だと思いますが。
Andrew: 確信しています。もちろん現時点では標準ライブラリにまだ整理しきれていない部分もありますし、言語自体もまだ1.0に達していません。しかしその方向性と設計上の意思決定——グローバルアロケータの廃止、遅延解析によるトップレベル宣言の未使用部分の非評価、コンパイル時実行——これらはすべて「どんな環境でも動く」という目標に向けて一貫して積み上げられてきたものです。どんなユースケースを提示されても、Zigがそれを想定していることを示せる自信があります。
7. 非同期処理——GoとCの両方のユースケースを同一コードベースで実現する「色盲な非同期関数」
7-1. CスタイルのブロッキングコードとGoスタイルのイベントループの違い
Andrew: 少し立ち戻って、非同期処理について整理しておきたいことがあります。Cコードを書く場合、最もシンプルな形ではOSのシステムコールを呼び出すブロッキングな命令型コードを書きます。デスクトップアプリケーションを想定したCコードであれば、これが基本的なスタイルです。よく理解された概念であり、特に複雑さはありません。一方、Goのコードを書く場合は常にイベントループが存在します。Goルーチンを起動するとイベントループが動き、M:Nスレッディングが行われる仕組みです。GoコードはCのスタイルとは根本的に異なるため、GoのライブラリをCから呼び出すことは非常に難しい。Goはイベントループを動かすための重厚なランタイムに依存しているからです。
Beyang: なるほど、CとGoではそもそもコードの動き方が根本的に違うわけですね。
Andrew: そうです。Rustはその両方に対応しています。ただしRustの場合、それぞれが別のコードベースになります。Rustではブロッキングな命令型コードを書いてOSのシステムコールを呼び出すCスタイルと、TokioなどのクレートでGoスタイルに近いイベントベースの非同期処理をするスタイルの両方が書けます。ただしこの2つは別々のコードベースとして存在していて、同じライブラリコードを両方のスタイルで共有するのは容易ではありません。
7-2. Rustによる両対応とZigの「色盲な非同期関数」——同一コードベースでの実現
Andrew: Zigが面白いのはここです。Rustと同様にZigも両方のユースケースをサポートしていますが、Zigでは同一のコードベースで実現できます。async/awaitの言語機能と、標準ライブラリのイベントループ実装を組み合わせることで、GoコードのようにM:Nスレッディングとイベントループを使うコードも書けますし、Cスタイルのブロッキング命令型コードも書けます。そしてその切り替えはコンパイルオプションで行えます。
この仕組みを私たちは「色盲な非同期関数(colorblind async functions)」と呼んでいます。async/awaitを使ったライブラリがあったとして、それはイベントループに依存しないまま書けます。なぜなら、同じコードをイベントモードをオンにしてコンパイルすれば理想的なイベント駆動コードが生成され、イベントモードをオフにしてコンパイルすればCスタイルのブロッキングコードが生成されるからです。どちらの場合でも理想的なコードが生成される。
Beyang: それはGoとの違いとして非常に重要ですね。Goはウェブサーバーを作ることを念頭に置いているのに対して、ZigはよりCに近い立ち位置で、OSのプリミティブな機能の上に直接構築されていて、その低レベルの機能を開発者にとってよりアクセスしやすい形で提供しようとしている——そういう理解で合っていますか。
Andrew: おおむね合っています。ZigはGoやRustとは異なるニッチを持っています。Goはほぼ明示的にウェブサーバー向けに作られていて、Rustは次世代のウェブブラウザのような用途を意識していると言えます。Zigはより根本的なところに位置していて、OS上で動くあらゆるものに対して、同一のコードベースで対応できることを目指しています。イベント駆動にもブロッキングにも、カーネルにもデスクトップアプリにも——それが「ジェネラルパーパス」という言葉の実質的な意味です。まだ実験的な機能ではありますが、async/awaitとイベントループの組み合わせは完全にサポートされています。
8. ZigとRustの比較——言語の複雑さと安全性戦略の根本的な違い
8-1. 言語の複雑さという軸——機能の多さ vs. 機能の少なさ
Beyang: ZigとRustはよく比較される存在だと思います。Stephenさんはかなりの量のRustコードを書かれてきたと思いますが、まずAndrewさんの視点から、ZigとRustの違いはどこにあると考えていますか。
Andrew: 直接答える前に、少し整理しておきたいことがあります。ZigとRustを比較するとき、大きく2つの軸があります。一つは言語の複雑さ、もう一つは安全性の戦略です。まず複雑さの軸から話すと、Rustは機能を積極的に採用する言語だと思います。ある種の問題を解決するための手段が複数あり、ライフタイムとトレイトが絡み合い、さらに他の言語機能と組み合わさる。言語機能が非常に豊富で、それぞれが様々なパターンに対して非常に便利です。ただその分、生産性を発揮するまでの学習コストが高くなります。他人のコードを読んでいて自分がまだ知らない機能が使われていたら、コードを読むのを一時中断してその機能を学びに行かなければならない。それ自体は悪いことではなく、一つの言語戦略です。
Zigの目標はその逆です。何かを達成する方法は一つだけであり、言語機能の数も非常に少ない。すべての機能を習得したら、他人のコードを読むときに言語の学習のために立ち止まる必要は一切なく、コードが何をしているかを理解することだけに集中できます。Zigのウェブサイトに「no implicit control flow(暗黙の制御フローなし)」という記述があります。これを読んだとき、読んだ人がZigプログラマーになるかどうかの分岐点になります。もっと機能が欲しいと思う人はRustやその他の言語を選べばいい。それぞれが自分の哲学に合った選択をすれば良いのです。
Stephen: 開発者体験として私が最も感じた違いはまさにそこです。Goを長年使ってきた経験から言うと、Zigの学習曲線はGoに非常に近いと感じました。数週間でGoを習得して変更を加えられるようになるのと同じ感覚で、Zigでも比較的短期間でほぼ全ての機能を把握できた感覚があります。数ヶ月しか使っていないのに、言語のほぼすべてを知っている感覚があります。一方でRustは2〜3年の間、断続的にコードを書いてきましたが、別のコードベースに飛び込むたびに、知らないマクロや知らない概念に出くわして「また別の言語を学ばないといけない」という感覚になります。見知らぬコードに自信を持って飛び込めるかどうか、その感覚の差が大きいです。
Andrew: それを聞いて本当に嬉しいです。Zigのウェブサイトに「プログラミング言語の知識ではなく、アプリケーションのデバッグに集中できる」と書いていますが、それが実際に体験として伝わっているということですから。
8-2. 安全性戦略の違い——Rustの「水平安全性」とZigの「垂直安全性」
Andrew: もう一つの軸が安全性の戦略です。私はこれを「水平安全性」と「垂直安全性」と呼んで説明しています。Rustにはunsafeブロックがあります。コードの特定の範囲をunsafeとして括り出して、そこには「ここに竜あり(here be dragons)」という大きな警告を立てる。そのunsafeブロックの外側はすべて安全であるとみなされる、という構造です。
Zigにはunsafeというキーワードが存在しません。Zigのどの行でも、整数をポインタにキャストしてそこから値をロードする、という操作が書けます。これは非常に危険な操作です。しかしそれが安全でないという誤解をしてほしくない。Zigには安全性への取り組みが言語全体に散りばめられています。非常に多くの作業が安全性のために費やされていて、それが言語のあちこちに存在しています。
Beyang: Zigの安全性は「ない」のではなく、「あるが、granularである」ということでしょうか。
Andrew: まさにその通りです。Zigのアプローチは粒度が細かい、垂直な安全性戦略です。unsafeブロックという形でまとめて括り出すのではなく、個々の操作レベルで安全チェックが入っている。コードベース全体で安全性の恩恵を受けながら、特定の操作においては意図的に危険なことができる、という構造です。
8-3. 安全チェックの具体例——ポインタのアライメント・整数オーバーフロー・配列の境界外アクセス
Andrew: 具体的に見ていくと、Zigにはたとえばポインタのアライメントチェックがあります。アライメントキャストを行う場合、それは安全チェック付きです。整数オーバーフローについては、ラッピング演算子を明示的に使わない限りオーバーフローは安全チェックの対象になります。配列の境界外アクセスについても安全チェックがあります。
一方でまだ検出されないケースもあります。たとえばスタック変数へのポインタを取得して、そのポインタが関数呼び出しより長生きしてしまうケースは現時点では検出されません。ただ言語はまだ1.0に達していないので、ローカル変数のuse-after-freeを含め、さらに多くの安全チェックを追加する計画があります。安全性はあちこちに散りばめられていて粒度が細かい、ということが重要な点です。
Rustとの対比で言えば、組み込みデバイスでメモリマップドI/Oのようにレジスタに書き込んだり読み出したりする、という本質的に危険な操作が必要な場面でも、Zigであれば他のすべての安全機能は引き続き有効なまま作業できます。白か黒かではなく、危険な操作をしながらも他の部分での安全性は維持できる。Rustの場合はunsafeを使った瞬間に、その範囲全体に対して竜を解き放ってしまうような感覚があります。それがZigの水平と垂直という表現の意味です。
Andrew: ただし正直に欠点も言っておきます。この設計はプログラマーとしての規律を要求します。そしてご存知の通り、プログラマーは規律があるとは言えません(笑)。Zigは安全でない抽象化を作ることもできてしまう。その場合、自分で自分の足を撃ち続けることになります。しかし言語はツールを提供しているので、安全な抽象化を作ることもできる。それをきちんと行えばコードベース全体が安全になります。
9. ZigがCやRustより速い理由——整数演算・アロケータ設計・データ指向プログラミングの実証
9-1. Cとの比較——符号付き/符号なし整数のオーバーフロー挙動の違いとコード生成の優位性
Beyang: Zigは最近セルフホストコンパイラに移行した、つまりコンパイラ自体がZigで書かれるようになったと理解していますが、以前はC++で書かれていたところからZigに移行してかなり速くなったと聞きました。ZigプログラムはCプログラムと比べて一般的に速いのでしょうか。
Andrew: ZigはCより速いと主張していますし、その主張に自信を持っています。マイクロベンチマークで整数演算だけを見ても、生成されるコードが優れていることを確認できます。さらに言語の設計原則、標準ライブラリの慣習や構造、そして実際のアプリケーションにおける結果を見ても、ZigプログラムはCより速く、さらにRustよりも速い傾向があると言えます。
Beyang: それはかなり大胆な主張ですね。Cは最も低レベルなものとして知られているのに、なぜZigがより速いのでしょうか。
Andrew: CとRustとでは理由が異なります。まずCとの比較から具体的な例を一つ挙げましょう。Cには奇妙な仕様があって、符号付き整数のオーバーフローは未定義動作ですが、符号なし整数の加算オーバーフローはラッピング動作として定義されています。Zigでは一貫しています。符号付きも符号なしも、オーバーフローは違法な動作です。もしオーバーフローしないと仮定することで違法な動作を最適化に活用する(つまり未定義動作として扱う)モードでコンパイルするなら、それが未定義動作になります。ラッピング演算が必要なら、明示的なラッピング演算子を使わなければならず、そうすれば意図通りの動作が得られます。
ポイントはここです。Cには符号なし整数に対して「オーバーフローしないと仮定する」加算演算子が存在しません。Zigではオプティマイザがそれを仮定できるので、より良いコードが生成される。これはZigの言語設計上の決定がより優れたコード生成につながるという、数ある例の一つに過ぎません。
9-2. アロケータ渡しがもたらすメモリ効率意識——zigglyphでのアリーナアロケータ実験(約2倍の速度向上)
Andrew: しかし実際により重要なのは、アロケータを明示的に渡す設計によってメモリの使い方に対する意識が自然と高まり、より効率的なパターンが便利な選択肢として浮かび上がってくる点です。Cでもこういったことは技術的には全部できます。Rustでも同様です。しかしZigではそれが慣習として組み込まれているため、開発者が自然にそういうパターンを使うようになります。
Stephen: 昨夜zigglyphというZigのUnicodeパッケージを見ていたとき、作者が汎用アロケータをアリーナアロケータに交換したところ、全ベンチマークで約2倍の速度向上が得られたケースを目にしました。データ構造単位でアロケータを差し替えられるという設計が、グローバルなアロケータを差し替えるような大掛かりな作業なしに、ピンポイントで大きな最適化をもたらすというのは非常に印象的です。
Andrew: まさにそれが良い例です。アロケータを引数として渡す設計にすることで、データ構造ごとに異なるアロケータ戦略を試すことが非常に容易になっています。これはどこかで見たことがない実践的な最適化アプローチだと思います。
9-3. データ指向プログラミングとCPUキャッシュの仮説・実験・検証——IRオブジェクト削減による35%高速化(×4箇所)
Beyang: セルフホストコンパイラへの移行で速くなったのは、ZigがC++より速いからということでしょうか。
Andrew: 少し補足が必要です。セルフホストコンパイラによる速度向上の大部分は、C++からZigに書き換えたこと自体よりも、2回目の実装として設計をやり直したことから来ています。何かの2回目の実装は常により良くなりますから、その分が速さに寄与しています。ではセルフホストコンパイラで実際に何を改善したのか、そこにこそZigが速い理由の本質があります。
私はデータ指向プログラミングについて学び、CPUのキャッシュシステムについてより直感的な理解を深めました。根本的な観察はこうです。「触れるメモリの量を減らせば、CPUキャッシュが実際に触れるメモリに対してより多くのヒットを維持できる」。これが基本的な観察で、この観察をもとに様々なコード変更を加えれば、コードが速くなります。
セルフホストコンパイラでこの原則を適用した具体的な箇所は、ヒープ上に多くのオブジェクトを生成している部分です。生成していたオブジェクトはIR命令(中間表現命令)でした。これはコンパイラの一つのコンポーネントから別のコンポーネントへの受け渡しに使われる中間表現で、ユーザーが書いたコードに基づいてメモリ上に大量に生成されます。
CPUキャッシュに関する観察とこの知識をもとに立てた仮説はこうです。「これらのオブジェクトが占めるメモリを削減すれば、コンパイラのメモリ使用量が減るだけでなく、CPUキャッシュへのプレッシャーが減って結果としてコードが速くなるはずだ」。この仮説は完全に正しいことが証明されました。結果として約35%の速度向上が得られました。さらに驚いたのは、まったく同じパターンがコンパイラの他の3箇所にも存在していたことです。同じ戦略を適用して、同じ規模の速度向上を3回繰り返し達成できました。
Beyang: キャッシュヒット率の改善がこれほど大きな速度向上をもたらすというのは、コンピュータサイエンスの授業ではアルゴリズムとビッグOの話ばかりで、あまり教わらない領域ですよね。
Andrew: そうです。これはコンピュータサイエンスというよりコンピュータの実装上の詳細の話です。しかし実際のパフォーマンスに対する影響は非常に大きい。
9-4. Rustのunsafeなしに同等の最適化を安全に実現——struct-of-arraysによるセルフホストコンパイラでの実証
Andrew: ここでRustとの比較に戻りたいと思います。このデータ指向設計の最適化を実現するためのコアコンポーネントの一つが、アンタグドユニオンです。Rustではこれはenumとして扱われますが、Rustのenumはタグ付きです。私がやりたかったのは、タグを持つけれどもそのタグを別の配列に分離する、というアプローチです。これがstruct-of-arraysと呼ばれるパターンです。
具体的には、一方の配列に各要素として1バイトのタグを持たせます。このバイトが命令の種類を表します。加算、減算、ストアといった種類です。もう一方の配列には命令のデータを格納し、各要素は固定サイズです。インデックス1番のタグを知りたければ、タグ配列のインデックス1を参照する。インデックス1番のデータが欲しければ、データ配列のインデックス1を参照する。この構造のポイントは、タグとデータを一つの配列にまとめようとすると、1バイトのタグの後ろにポインタサイズのフィールドが来る場合、7バイトのパディングが必要になることです。配列を分けることでそのパディングが消えます。パディングをCPUキャッシュに載せることがなくなり、キャッシュを無駄遣いしなくなる——これがキャッシュ効率の改善につながります。
問題はこれをRustでunsafeなしに実現できないことです。unsafeを使えば技術的には可能ですが、Rustコミュニティではunsafeコードを書くと批判を受ける文化があります。「unsafe」という言葉自体が重く受け止められる言葉ですし、実際に人々はsafeなRustコードを書こうとします。しかし完全にsafeなRustコードではコンピュータのハードウェアを完全に活用できないケースがある。
Zigではこのコードが安全です。アンタグドユニオンに対しても安全チェックがあるからです。安全性を諦めることなくこの最適化を実現できる。これはZigの設計がより大局的なアプローチを取ってきた結果です——Rustのようにすべてが一貫した普遍的なスキームに収まらなければならないのではなく、個々のケースに対してより良い解を積み上げてきたことで実現できた点です。
ZigセルフホストコンパイラはZigプログラムとして作られた実際のユースケースであり、このstruct-of-arraysパターンはそこで実証されました。Rustで同じコードを書こうとするとunsafeを使うか性能を諦めるかの二択になりますが、Zigでは安全かつ高速に実現できる——これがZigがRustより速いと主張する根拠の一つです。
10. 開発者体験——学習曲線とデバッグ哲学
10-1. Stephenの証言——数ヶ月でZigをほぼ習得できた学習曲線とGoとの比較
Beyang: パフォーマンス特性の話をしてきましたが、次は開発者体験の話に移りましょう。Stephenさんは長年Goを使ってきて、Rustも断続的に触れてきた中でZigに移ってきたわけですが、その学習曲線や開発者体験はどうでしたか。
Stephen: 私にとって最も重要なのはオンボーディング体験、つまり最初の学習曲線です。GoからZigに移ってきた経験から言うと、それほど大きなギャップはありませんでした。Goであれば、おそらく1週間ほどで基本を把握して変更を加えられる感覚になれると思います。Zigも同様で、数ヶ月使っただけで言語のほぼすべてを知っている感覚があります。他の言語のバックグラウンドを持つ人は異なる体験をするかもしれませんが、Goを長年使ってきた自分にとっては非常に自然な移行でした。
さらに面白かったのは、学習の連続性です。Zigの基本を学んだ後にコンパイル時実行の概念に触れたとき、最初は「なるほど、面白い。でもどう感じるかはまだわからない」と思いました。しかしジェネリックなデータ構造を書きたくなったとき、「ああ、これはさっき学んだことのまったく自然な拡張に過ぎない」と気づきました。型をパラメータとして渡すだけで、まったく同じ書き方ができる。その流れが非常に自然な学習曲線を生み出していました。C++の場合は基本的なコンパイルを学んでからプリプロセッサシステム全体を別途学ばなければならない、という断絶があると思いますが、Zigではその断絶がありませんでした。
10-2. Rustの学習コスト——他人のコードを読む際に都度学習が必要なマクロや機能
Stephen: Rustとの比較で言うと、私は2〜3年の間断続的にRustのコードを書いてきましたが、別のコードベースに飛び込むたびに「ここに見慣れないマクロがある」「これは自分がまだ知らない概念だ」という体験を繰り返してきました。外部のコードベースに自信を持って飛び込んでいける感覚が、Rustではなかなか得られなかったのです。コードが何をしているかを理解することに集中したいのに、都度言語の学習のために立ち止まらなければならない。その摩擦が積み重なります。
Andrew: Rustは機能を積極的に採用する言語で、ある問題を解決する手段が複数存在します。ライフタイムとトレイトが絡み合い、さらに他の言語機能と組み合わさる。それぞれの言語機能は非常に便利で強力ですが、すべてを習得するまでのオーバーヘッドが大きい。Zigの目標はその逆で、何かを達成する方法は一つだけ、言語機能の数も非常に少ない。言語機能をすべて習得したら、他人のコードを読む際に言語のために立ち止まる必要は一切なく、コードが何をしているかを理解することだけに集中できます。これはトレードオフであり、どちらが正しいかではなく、どちらの哲学に共感するかの問題です。
10-3. Zigの設計目標——「プログラミング言語知識ではなくアプリケーションのデバッグに集中できる」
Andrew: Zigのホームページには「プログラミング言語の知識ではなく、アプリケーションのデバッグに集中できる」という一文があります。これはZigの設計哲学を端的に表しています。言語が複雑であればあるほど、デバッグの際に「これはバグなのか、それとも自分が言語の挙動を誤解しているのか」という問いに時間を取られます。Zigではその問いが生まれにくい設計になっています。
Stephen: それは実際に体験として感じています。Zigでコードを書いていると、問題が起きたときに「このコードは何をしているのか」という問いに集中できる。言語のせいなのかコードのせいなのかという区別に悩む時間が短い。それが開発のリズムに大きく影響します。Zigに移ってきてから生産性が高いと感じる一因はそこにあると思います。
Andrew: Stephenさんからそう言っていただけるのは本当に嬉しいです。それがまさに私たちが目指していることで、ホームページにそう書いているからといって実際にそうなっているとは限りませんから。実際に使っている人がそう感じてくれているというのが一番の証明です。言語機能を最小限に抑えることは、新機能を追加することよりずっと難しい判断を要します。何かを省くことには常に圧力がかかりますが、その圧力に抗い続けることでこの体験が実現できています。
11. 採用事例とコミュニティへの呼びかけ
11-1. 注目のオープンソースプロジェクト——Linuxウィンドウマネージャ「River」
Beyang: 残り時間も少なくなってきましたので、Zigを実際に使っているプロジェクトや組織、企業の事例について教えてください。
Andrew: 二つの観点からお答えします。まずオープンソースプロジェクトとして紹介したいのが「River」です。検索すればすぐに見つかると思います。Isaac FerrandによるLinux向けのウィンドウマネージャで、Zigで書かれています。Zigがシステムレベルのプログラミングに実際に使われている具体的な例として、ぜひ見てみてください。
11-2. 商業利用の例——金融会計データベース「TigerBeetle」(Coil HQ)
Andrew: もう一つ紹介したいのが「TigerBeetle」です。名前を正確に確認させてください——はい、TigerBeetleで合っています。Coil HQという会社が開発している、新興の金融会計データベースです。商業プロジェクトとしてZigを採用している事例として非常に興味深いと思います。金融システムという高い信頼性と性能が求められる領域でZigが選ばれているという点は、言語の実用性を示す強力な証拠です。
11-3. Zig Software Foundationへの寄付のお願い
Beyang: この番組を聞いている方々に向けて、エピソードを聞き終わった直後にやってほしいことが一つあるとすれば何でしょうか。
Andrew: お金をください、とお願いしたいです(笑)。私たちがやっていることを気に入ってくれたなら、ぜひ支援をお願いします。Zigには多くの人がボランティアとして貢献してくれていて、素晴らしい仕事をしてくれています。しかし寄付が足りないため、彼らに十分な報酬を払えていません。寄付が増えれば、その分だけ貢献者に対して報酬を払えるようになります。ziglang.orgから寄付をしていただけると非常に助かります。
Beyang: 私も寄付します。Andrewさん、本当に素晴らしいプロジェクトに取り組まれていますね。今日は時間を割いていただきありがとうございました。非常に勉強になりました。
Andrew: とても楽しかったです。ありがとうございました。
Stephen: お会いできて光栄でした。
Andrew: こちらこそ、Stephenさんにお会いできて嬉しかったです。ありがとうございました。
