世界を動かす技術を、日本語で。

私はZigの新しいIOインターフェースには向いていない

概要

  • Zig 0.15で導入された新しいIOインターフェースとその課題の整理
  • std.Io.Readerおよびstd.Io.Writerの型変更による影響
  • tls.Clientの初期化やバッファ利用に関する実装例と問題点
  • オプション引数の一貫性やドキュメント不足による混乱
  • 実際のコード例と遭遇したエラー・未解決点の共有

Zig 0.15の新IOインターフェースとtls.Clientの利用

  • Zig 0.15新しいIOインターフェース (std.Io.Reader、std.Io.Writer)が導入

  • 旧インターフェースは パフォーマンス問題型の混在anytype依存 が課題

  • 新インターフェースは バッファリング を前提とした設計

  • tls.Client.init は新しいReader/Writer型とオプションを要求

  • net.Stream から reader()/writer() でReader/Writerを取得し、 interface()&interface で型変換

    • Reader: reader.interface() で*std.Io.Readerへ
    • Writer: &writer.interface で*std.Io.Writerへ
  • バッファstd.crypto.tls.max_ciphertext_record_len のサイズが推奨

  • writer/reader のアドレスは安定している必要があり、ヒープにラップする設計が現実的

tls.Client.initのオプションとバッファ管理

  • tls.Client.init は4つのオプションが必須
    • ca_bundle (証明書バンドル)
    • host (接続先ホスト名)
    • write_buffer
    • read_buffer
  • オプション引数に 必須パラメータ が混在しており、一貫性に疑問
  • バッファは &write_buf2&read_buf2 のように指定
  • stream.writer/reader にもバッファを渡す必要あり

実装例と遭遇した問題

  • GETリクエスト送信後、レスポンス取得時に std.Io.Readerにreadメソッドが存在しない
  • streamメソッドstream to writer という形でデータ取得を試みる必要
  • バッファサイズや指定方法を間違えると アサーションエラープログラムのハング が発生
  • 完全な動作例は見つからず、 Zigのドキュメント不足サンプルコードの少なさ が障壁
  • tls.Client.reader は復号済みデータだが、使い方が直感的ではない

Zigの設計・ドキュメントに感じる疑問

  • オプション引数の設計Reader/WriterのAPI設計 に一貫性の不足を感じる
  • read(buf: []u8) !usize のような直感的なAPIが存在しない
  • ドキュメントやテストケース が少なく、実装時に迷うポイントが多い
  • 旧APIからの移行で 基本的な関数名変更 にも苦労(例:printInt)

まとめと今後の課題

  • Zig 0.15の新IO設計 はパフォーマンスやバッファリング重視の一方、 直感的な使いやすさやドキュメント が課題
  • tls.Clientの利用 にはバッファ管理や型変換、オプション指定の理解が必須
  • コミュニティやソースコード を参考にしつつ、今後のAPI改善やドキュメント充実に期待

Hackerたちの意見

ライブラリやインターフェースが、タイプのためにバッファを割り当てることを求めるのが理解できないんだよね。解析できないし(そしたらそのライブラリは必要ないし)、書き込むこともできない(交換が壊れちゃうかも)。Goの奇妙なインターフェースは、いくつかのインターフェースがハイジャッカーインターフェース(ResponseWriter.(http.Hijacker))みたいにライターを拡張するために使えるからだと思う。リクエストオブジェクトは、異なるミドルウェアとやり取りしながら何度も使われるし。要するに、リクエストは拡張する必要はないけど、レスポンスはWebSocketやラップされたTCP接続、他の何かになる可能性があるってこと。

これは単に、ラジアンと度みたいな別の慣習だね。任意のIOに出入りできるし、ある言語では一方向をモックと呼び、別の言語では逆をunsafeFooと呼ぶ。アンドリュー・ケリーは、Haskellの最高の頭脳たちが論文を書いてきた30年をライブストリームで独立して再発見したんだ。だから、未来はZigだね。彼が最初にたどり着いた。

外部バッファの目的って、関数がメモリを割り当てる必要がないことじゃないの?

「ライブラリやインターフェースが自分でバッファを割り当てることを求めるのが理解できない。」それ、そんなに変だとは思わないな。トレードオフだよね:柔軟性は増すけど、手作業が増える。例えば、もう使っていないバッファを持っていて(バッファプールがあると仮定して)、再利用したい場合、その型が裏で自分で割り当てちゃうとできないんだよね。あるいは、リソースを事前に静的に割り当てる必要がある環境で作業していて、後から割り当てられないこともある。大きな欠点は、90%の人が単にバッファを割り当てて渡すだけなのに、90%の人が余計な作業をしなきゃいけないってこと。実際に必要なのは10%の人だけなのにね。理想は、たくさんの柔軟性を持たせつつ、シンプルで一般的なケースを簡単にすること。インターフェースを改善する簡単な方法は、呼び出し元がゼロ長のバッファ(またはZigのnullのバージョン)を渡せるようにして、その型が自分でバッファを割り当てるようにすることかも。もちろん、それをできるってことを人々が知るためのドキュメントの負担はまだ残るけどね。もう一つの選択肢は、バッファ引数を全く取らない第二のコンストラクタ関数を作って、そこがバッファを割り当てて、完全に柔軟なコンストラクタ関数に渡すっていうのもありかも。

Zigの言語は本当に良いけど、標準ライブラリはまだまだ進行中で、常に変わってるし、いろいろな部分が欠けてるし、ところどころ抽象化が過剰で、他のところでは低レベルすぎる。今は標準ライブラリは避けて、OSのAPIを使った方がいいと思う。ベータテスターになる覚悟があるなら別だけど。

そうそう、俺もほとんどいつもゼグをこう使ってる: 古いOSのAPIを使うだけで、cImportsを使うのはすごく簡単だし、ゼグの定義を作るのが面倒なときは特にね。

私の視点から見ると、Zigはあまりにも多くのことをやろうとしていて、私が受け入れられる基準に達することはないと思う。彼らは、独裁者の気まぐれで変化を強いられるユーザーに対して、かなり失礼だと思う。十分な人が受け入れて、壊れたツールでも1.0が出るまでは大丈夫だと認めてしまったから、今はその明らかな欠点を見過ごして、来るユートピアを期待している(ネタバレ:その日は絶対に来ない)。個人的には、他の人に自分の実験を押し付けるのは間違ってると思うし、例えば rug を引き抜いた時に「不安定だって言ったじゃん、最初から依存しない方が良かったんだよ」って言うのもおかしい。Zigが何を目指しているのかも全然わからない。マトクラッドは、Zigは機械レベルの言語だと思ってるみたいだけど、公式の言語のランディングページでは「Zigは堅牢で最適化された再利用可能なソフトウェアを維持するための汎用プログラミング言語およびツールチェーン」と書いてある。この二つの定義は互いに矛盾してるよね。さらに、Zigは明らかに汎用言語ではない。手動のメモリ管理が必要でも望ましくもないプログラミング問題はたくさんあるからね。この混乱はZigの不安定さや膨れ上がった標準ライブラリに現れている。実際、大きな標準ライブラリは、彼らがしばしば主張するシンプルさや汎用性と矛盾している。非同期は、さまざまなプラットフォームが持つ能力の根本的な違いから、オーバーヘッドや間接的な処理を追加せずに普遍的に実装できる機能ではない。再び、彼らは銀の弾丸を約束しているけど、以前の試みでは関数の色付けが解決されたと公言したのに、それが放棄された。どうして二度目に彼らを信じることができるの?コンパイラを実装するために必要なアセンブリプリミティブは、すべてのプラットフォームが提供する数少ないものだけだ。ロード/ストア/mov/inc/jeq/jump、あとはいくつかかも。Luajitは純粋なアセンブリでパーサーを実装していて、Luajitが動く重要なプラットフォームでZigが動くのは知らない。私はほとんどのプログラミングをLuaでやってるけど、インタープリターでバグに遭遇したことは一度もない。ZigがLuajitよりも優れた解決策を提供する問題を一つも思いつけない。もしそれが存在したとしても、ZigのコードをLuaファイルに埋め込んで、Luaを使ってZigコンパイラを動かし、特化したコードをLuaのffiで呼び出すことができる。でも、ほとんどのコードは、Zigを採用することで生じる他の頭痛の種を我慢するほど、機械コードレベルに最適化する必要がない。Zigに関する期待は、現実からの乖離が本当にLLMレベルに達している。Zigを信じるには、今は持っていない能力が魔法のように開発されると信じなければならないし、それを実行するための具体的な計画はない。ただ「待ってて」とかいう漠然とした計画だけ。

ほとんどドキュメントの問題か、ドキュメントがないことが問題だね。

ゼグのいろんな部分が毎月変わるから、ドキュメントを書くことが優先されてないみたい。ゼグのチュートリアルも「ゼグコードの例がたくさんある」って感じで、最新のコンパイラでは必ずしもコンパイルできるわけじゃないし、スタンダードライブラリの定義を読むことが求められる。シンプルな呼び出しの場合は、ゼグの構文のコツを知ってれば結構うまくいく。多くの要件や含意は、論理的に名前が付けられた短い関数に書かれてることが多い。アロケーターみたいな概念は、実際に自分で書きたくないかもしれないけど、概念的には簡単だよね。でも複雑な概念に触れると全然ダメになる。ゼグの新しいI/Oシステムは、Javaのストリームやラッパー、リーダー、ライターに似ていて、暗号化されたテキストを安全なチャネルで送るのがoutput.write("hello")みたいに簡単になる。新しいI/Oシステムと、それを使うためのドキュメントが不足してるのは間違いだと思う。こんなに複雑な型システムをゼグのスタンダードライブラリで表現するのが良いアイデアかどうかもわからない。言語全体が明確で簡潔、短くて読みやすいメソッドで動いてるのに、新しいシステムはその点でイディオマティックじゃない気がする。

私はZigのPMじゃないけど、OPが書いた問題の明らかな解決策は、使い方の例を含むより良いドキュメントを書くことだと思う(多ければ多いほどいい、ほとんど欠点になるくらい)。ユーザーがやりすぎてないかを考える良い機会にもなるし。もしトレードオフが絶対的なパフォーマンスや負荷をかけるパフォーマンス低下の抽象化を避けることだったら、その目標は達成されたと思うけど、DXは犠牲になったかもね。

Zigの文化に詳しくないんだね。ドキュメントの不足について文句を言うと、今Zigを書いてるほとんどの人から「stdlibのコードを読め」っていう助けになるコメントが大量に返ってくる準備をしておいた方がいいよ。ほとんどのAPIはこの投稿と同じくらい使いにくいから(HTTPや基本的なファイルシステム操作をチェックしてみて)、生き残るのは最強の者だけだよ。

Zigは、何をすべきか、どうすべきかのバリエーションを集めて教えるのではなく、何をしないべきかの指示を出すことに偏りすぎてると思う。このインターフェースのドキュメント不足はその良い例だね。

良いドキュメントや例を書くのって、すごく手間がかかるよね。今のZigの状況を考えると、無駄になっちゃう気がする。

ドキュメントを書くにはコストがかかるんだよね。時間が必要で、その時間をZigの他の部分を改善するのに使えるかもしれないし。進行中のコードに関しては、もう少し落ち着くまでドキュメントを作らない方がいいこともあるよね。もちろん、ドキュメントは大事だけど、新機能、重要なバグ修正、ドキュメントのどれかを優先しなきゃいけないとき、全部は無理だよね。

私はZigの開発者じゃないけど、Zigのドキュメントがシンプルなのは、言語がまだ若くて常に進化しているからなんじゃないかなって思う。書いたことが将来的に間違ってしまうかもしれないってわかっていると、ドキュメントを書くのに時間とエネルギーを割くのがすごく難しいよね。

それは悪いことだよ。実行の境界を全体のランタイムエンジンに混ぜ込んで、どのようにそれぞれを橋渡しするかを明示的に示していないから。

Zigは低レベルなことをやるには最適な言語だと思う。C/C++のクロスコンパイラとして使えるっていうのは素晴らしいよね。

作者です。やっと動くようになったよ。暗号化されたライターとストリームライターの両方をフラッシュする必要があった。読み取りにもいくつか問題があったけど、ストリーミングは動く。ただ、最初の読み取りでは常に0が返ってくるんだ。Writer.FixedがsendFileを実装してないから、最初の呼び出しの後に内部的にストリーミングモードから読み取りモード(1)に切り替わって、そこからはうまくいくようになる。今は、ウェブソケットライブラリで圧縮を再度有効にしようとしてるところ。(1) https://github.com/ziglang/zig/blob/47a2f2ddae9cc47ff6df7a71...

ハハ、「フラッシュを忘れずに」 https://www.youtube.com/watch?v=f30PceqQWko

最小驚きの原則はどうなったんだ?

前のインターフェースからこれに移行するのは、確かに何かあるね。うわぁ。

俺は… ゼグのサイドプロジェクトを0.15.xにアップデートするつもりはない。アンドリューがこれをリリースしたかった理由はわかるし、新しいIoをみんなに届けたい気持ちも理解できるけど、リーダーやライターに大きな変更を加えたばかりの数週間後なんだよね。スタンダードライブラリに取り組んでる人にはいいことだと思うけど、ゼグをカジュアルに使ってる俺みたいな人間には、0.16.0が出てIOのゴタゴタが落ち着くのを待つのが正しい選択に感じる。

ゼグっていう言語なら、たまにはザグしなきゃいけないこともあるよね。

Zigのコアメンバーの一人、ロリス・クロも最近のインタビューで言ってたけど、IOの変更の影響が落ち着くまで待ってから、自分のプロジェクトを更新して続けるつもりらしいよ。でも、その後の未来は明るそうだね。アンドリューもロリスも、これが最後の大きな破壊的変更だと思ってるみたいだから、1.0がそんなに遠くないうちに見られるといいな。今一番不安なのは、スタックレスコルーチンを(再)導入した場合の影響だね。

Stream.Readerをstd.Io.Readerに変換するには、interface()メソッドを呼び出す必要がある。Stream.Writerからstd.io.Writerを取得するには、その&interfaceフィールドのアドレスが必要だ。これってあまり一貫性がないように思える。これをGoでどう受け入れられるか考えたら(おそらく却下されるだろうね)。彼らは変更を深く分析して、間違いを避けて一貫した解決策に到達するために必要なだけ時間をかけるアプローチを取ってる。これが俺のお気に入りの一つ: https://github.com/golang/go/issues/45624 4年かけて比較的マイナーなことを決定するのに、今はちょっとしたワンライナーの追加作業でできることなんだ。でも、物事はしっかり考えられないといけない。不整合が指摘されるし、デザインの懸念も提起される。実際のコード使用が考慮される… ちょっと遅すぎる人もいるかもしれないけど、俺はちょうど必要なスピードだと思う。最終的な決定はとても良い感じになりそう。

ラストも同じだね。便利な機能がたくさん実装されてるのに、ナイトリーバージョンでしか使えないのがちょっとイライラする。ジェネレーターみたいなやつ。でも、ラストが安定版に機能を出すときは、だいたいしっかり考えられてる。俺はせっかちだけど、ラストの言語とコンパイラのチームは正しい考え方をしてると思う。

Go 1.0の前は、必ずしもそうではなかったよね。かなり急速に変わったけど、根本的な変更ではなかった(例えば、セミコロンを削除したり、エラータイプを変更したりなど)。通常はコードを自動的に変換するオプションも提供してたし。今は1.0の後に安定すると約束したから、そうなってるんだ。

新しいIOインターフェースについての投稿を見て、ゼグから距離を置くことにした。なんかその直感が正しかったみたいで、これはC++11以前の冗長さに似てるけど、違う理由で同じ結果になってる。こういうパターン(新しい言語が置き換えるはずだった言語と同じくらい複雑になる)は見覚えがある。

あなたが言った投稿の著者の一人として、そんな投稿が人々を試すことから怖がらせるべきではないと思う。Zigチームは、より良い解決策が見つかれば議論して壊すことに熱心だよ。そんなアプローチは、Zigを将来の仕事の機会として考えている人にはあまり合わないけど、個人や小規模なチームのプロジェクトには、すでに目標が明確で素晴らしいツールを持ったいい言語だと思うよ。