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

プロトバッファは間違っている (2018)

概要

  • Protocol Buffers(protobuffers) の設計と実装に対する批判的な考察
  • 型システムの欠陥合成性のなさ といった構造的問題の指摘
  • 後方互換性・前方互換性 の主張に対する反論
  • Googleの内部実装 における課題の具体例
  • より良い設計案 の提案と現状の問題点のまとめ

Protocol Buffers(protobuffers)批判

  • protobuffersアマチュア的アドホック な設計による問題の温床
    • Google内部 でも同じ問題が発生
  • 型システムの貧弱さ が最大の課題
    • Javaの型システムに似ており、静的型付け・動的型付けの両者から不満
  • 多くの仕様が 後付け で追加されており、 一貫性・合成性 に欠ける
  • 既知の問題 に対して、 既に解決されている設計原則 を無視した対応

合成性の欠如

  • oneof フィールドは 繰り返し不可 という制約
  • map<k,v> の専用構文は他の型では使えない
  • ユーザー定義型 のパラメータ化ができず、汎用データ構造の再実装が必要
  • map のキーに使える型が限定的(stringのみ、enum不可)
  • map の値に他のmapを使えない
  • これらは 設計上の一貫性のなさ後付け仕様 の弊害

改善案の提案

  • すべてのフィールドを 必須 にし、 積型 として扱う
  • oneof独立した型(余積型) として昇格
  • 積型・余積型 を型パラメータで拡張可能に
  • これにより 任意のデータ構造 をシンプルに表現可能

スカラー型とメッセージ型の問題

  • スカラー型 は常に値が存在し、 未設定とデフォルト値の区別が不可能
  • メッセージ型 は内部的に有無の状態を持つが、アクセス方法が複雑
    • フィールド未設定時にデフォルト初期化が返される仕様
    • msg.foo = msg.foo; という代入が副作用を持ち、 直感に反する動作
  • has_foo() のようなメソッドで有無を判定する必要
  • これらの仕様により 抽象化・汎用化が困難

oneofフィールドの実装上の問題

  • oneof は本来 余積型 となるべきだが、実態は 排他的なオプション集合
  • セッターが他のフィールドを自動的にunsetするため、 予期しないデータ消失 の原因
  • 法則に則ったPrismやLens としての利用が不可能
  • 型安全・ポリモーフィックな操作 が実質的に不可能

後方互換性・前方互換性の幻想

  • protobuffersは データの意味的な保証をしない ことで互換性を主張
  • すべてがオプション扱いとなり、 不正なデータも型検査を通過
  • 防御的コーディング が全コードベースに拡散し、 集中管理が困難
  • 未知フィールドの保持 も現実的なアプリケーションではほぼ活用されない
  • DRY原則 を否定し、定義のインライン化を推奨
    • 将来的な分岐のために 再利用性・保守性を犠牲

より良い設計への示唆

  • 現代的な型システム の理解と適用があれば、仕様の大幅な簡素化と制約の撤廃が可能
  • 必須フィールド・積型・余積型・型パラメータ の導入による柔軟なデータ表現
  • 抽象化・型安全性・再利用性 を重視した設計の重要性

Hackerたちの意見

最初の一行も終わらないうちに「明らかに素人が書いたものだ」とか言ってる。これ、ただの怒りを煽るためのもので、読む価値ないよ。

自分の意見を伝える最良の方法は、相手を攻撃して自分の優れた知性を主張することだね。

そうそう、この記事は名誉の殿堂級の複合的誤謬で始まる。誰も主張してない仮想のアドホミネムを反論するストローマン。著者が一年以内にいくつかの大手テック企業から追い出された理由がなんとなく分かるね、LinkedInによると。

この記事が問題の詳細な分析と解決策も提供してくれたらいいのに。あ、実際そうだ!読んでみるべきだよ。

アマチュアが書いたものだけど、Google(世界で一番大きくて進んだテック企業の一つ)だけが抱える問題を解決してるよ。

そのラインの理由は根本的な緊張を示している。デビッド・ウィーラーが有名な言葉で言ったように、「コンピュータサイエンスのすべての問題は別の間接的なレベルで解決できる。ただし、間接的なものが多すぎる問題を除いて。」時間が経つにつれて、ますます巧妙な抽象が蓄積されていく。内面的に取り入れた抽象は見えなくなってしまう。それが私たちのやり方になって、他の人にどんなコストを強いているのか分からなくなる。すべての抽象は漏れがあるし、すべての抽象はメンテナンスプログラマーにとって障壁になる。これが、ブライアン・カーニハンが警告した問題につながる。「誰もがデバッグはプログラムを書くのの2倍難しいことを知っている。だから、書くときにできるだけ賢くなったら、どうやってデバッグするんだ?」結局、デバッグしなきゃいけないのは、あなたの抽象を知らないメンテナンスプログラマーだろう。Googleのアプローチから見える重要な知恵の一つは、業界全体が抽象に対して持つ傾向が有害だということ。特定の抽象が強力であっても、あまりにも多くなるとそれ自体が問題になる。だから、例えばGoは過剰な抽象を強く抑制するように設計されている。プロトバッファーは、言っている通りのことをする。意図されたシンプルな使い方をしていれば、うまく機能する。彼の不満は結局、「新しい抽象を生成するためにメタ操作を試みたけど、デザインがそれを許さなかった」ってことに集約される。それはアマチュアが書いたからではなく、ほとんどのプログラマーが無視できるほど賢いと思っているエンジニアリングの知恵を取り入れるために書かれているからだ。(過去の自分もそのプログラマーの一人だった。)技術は悪用されることもあるし、バカなことをする人もいるし、やりたいことができないこともある。もちろん。でも、KISSを守れば、うまくいく。シンプルに保てば保つほど、うまく機能する。これが、より良いエンジニアリングデザインを生み出すためのインセンティブだと思ってる。

if (m_foo = null) Googleをアマチュア呼ばわりして、その書いたコードが代入と比較演算子を区別できない一年生のエラーを含んでるなんて想像してみて。プログラマーが基礎的な技術について文句を言うクラスの愚痴がネット上にはあって、スキルの問題を認める代わりにそういうことを言ってる。もしその穴に深く入り込むと、最終的にはRustでカーネルを書き直す羽目になるよ。

うん、記事には皮肉がたくさんあって、彼らの主張を台無しにしてるよね。

プロトコルバッファはクソだけど、他のものも同じくらいクソだよね。逆互換性を保ちながら変更できることを定義して、逆互換性のある変更を強制するリンターがあるシリアライズ宣言フォーマット、他に何かある?その二つの条件だけで、せいぜい六つのフォーマットに絞られるけど、その中でプロトコルバッファが一番使われてる。記事には逆互換性のあるものは誰も使ってないって書いてあるけど、俺にはそれが奇妙に思える。プロトコルバッファを使って通信するN個のクライアントとサーバーを設定して、スキーマにフィールドを追加して、サーバーとクライアントをどの順番でもデプロイできるのは、他のフォーマットよりずっと楽だよ。プロトがクソなのはリモートプロシージャコールがクソだからで、プロトはそのクソさを隠そうとせずにむしろ露呈させるから。プロトや他の代替案に取り組んでいる人たちが改善を続けてくれることを願ってるけど、今使わないよりはマシだよ。

あまり広く使われてないけど、Typicalのアプローチは好きだな。https://github.com/stepchowfun/typical > Typicalは、互換性を壊さずにレコードタイプにフィールドを安全に追加または削除するための古典的な問題に対する新しい解決策(「非対称」フィールド)を提供してる。この非対称フィールドの概念は、和集合型でケースを追加または削除する際に互換性を保つという二重の問題も解決するんだ。

好きってわけじゃないけど、SBE(シンプルバイナリエンコーディング)は、逆互換性と前方互換性の領域ではまあまあの解決策だね。

ASN.1はメッセージのバージョニングを非常に正確に実装してる。リンターを実装するのは簡単だよ。

Hacker Newsで議論の続きを見る