
2025/12/08 1:55
Semantic Compression (2014)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
著者は従業員、マネージャー、契約社員のための複雑な継承階層を含む従来の C++ オブジェクト指向パターンを過剰で型安全性に欠けると批判しています。彼は「圧縮志向プログラミング」手法を提案し、まず具体的でケース固有のコードを書き、少なくとも二つ以上の類似インスタンスが現れた時だけリファクタリングする(「コードを早期に再利用可能にしない」)と述べています。マネージャークラスをそのベースにテンプレート化すると多重継承の衝突を解消できます。著者は長年続く不十分な OOP 習慣への苛立ちから、この実用的な姿勢を取るようになりました。
彼は The Witness エディタ UI にこのアプローチを示し、共通レイアウトロジックを構造体とヘルパー関数(Panel_Layout、row()、window_title()、push_button())に抽出し、計算をコンストラクタへ移動させ、共有スタックフレームを作成しています。結果として、各ボタン追加がわずか二行で済む簡潔な UI コードになります。complete()
この手法は重複を削減しつつ、コード全体のライフサイクルにおいて人間の労力を低く保ちます。今後の記事では、同じ再利用可能構造を使ってさらに多くの UI 機能を追加するために、この圧縮技術を拡張していきます。
本文
イントロダクション
C++ のプログラミング方法を皆さんが知っていることは間違いありません。
つまり、最初に言語を定義した顔立ちの濃い仲間たちによる素晴らしい書籍を読んだ経験があるということでしょう。その結果、実際の問題を解決する C++ コードを書くためのベストプラクティスを学びました。
まずは「給与計算システム」のような現実世界の問題を見てみます。そこには複数形の名詞がいくつかあります:「employees」「managers」などです。したがって、最初にすべきことはそれぞれの名詞に対してクラスを作ることです。少なくとも「employee」クラスと「manager」クラスが必要になります。
しかし本質的には両者とも人間です。そこで「person」という基底クラスを設け、従業員かマネージャーかに関係なく、人として扱えるようにします。これは非常にヒューマンであり、他のクラスが企業機械の歯車のように感じられないようにする効果があります。
ただし問題が残ります。マネージャーは従業員でもあるはずです。つまり manager は employee から継承すべきであり、employee は person から継承すべきでしょう。ここで本当にやりたいことに近づきます。実際のコードを書いていないものの、オブジェクトをモデル化することで、後は自然とコードが書けるようになります。
ところが……あれ? コントラクター(契約社員)もいるかもしれません。従業員ではありませんから「contractor」クラスが必要です。 contractor は person から継承できるでしょう。これなら非常にシンプルに思えます。
しかしマネージャーはどちらのクラスから継承すべきか?
- employee から継承すると、契約ベースで働くマネージャーが作れません。
- contractor から継承すると、フルタイムのマネージャーが作れません。
これは Simplex アルゴリズムに匹敵するほど難しい問題です。
一度は manager を両方から継承して片方だけを使わないという手も考えましたが、それでは型安全性が足りません。JavaScript のようなスクリプト言語ではなく、C++ で安全にやる必要があります。そこで解決策として、manager クラスをテンプレート化します。ベースクラスをテンプレートパラメータとし、manager を扱う全てのコードも同様にテンプレート化するのです。
これが最高の給与計算システムになるでしょう! すべてのクラスとテンプレートを設計したら、エディタを起動して UML 図を書き始めます。
プログラマが投稿するプログラミング記事
先ほど書いた内容が風刺的であったならいいのですが、残念ながら実際に世界中にはこのように考えるプログラマが多く存在します。
私は「Bob the Intern」のことではなく、有名な講師や著者を含むさまざまなプログラマを指しています。また、私自身もかつては同じ思考パターンに陥った経験があります。18 歳でオブジェクト指向プログラミング(OOP)に触れ、24 歳まで「すべてが馬鹿げている」と悟るまでに時間がかかりました。この気づきは RAD Game Tools の仕事を通じて得たものです。
多くのプログラマがこのような悪いフェーズを経て最終的には効率的に良いコードを書く結論へと至ります。しかし、教育資料の現状は「客観的に悪い」カテゴリに大きく偏っています。これは、優れたプログラミングは方法を知れば非常に直感的であるため、数式や高度なアルゴリズムのように「注目すべき」ものではないからだと考えます。その結果、多くの経験豊富なプログラマが自身のコードを書くことについて投稿しません。
そうすべきです。特別でなくても必要なことであり、良いプログラミングを投稿しない限り、人々は「悪いオブジェクト指向コードを書き続ける」状態から抜け出せません。
そこで次の Witness シリーズでは、実際にコードをコンピュータへ投入する純粋な機械的プロセスについて語ります。特別なアルゴリズムや数式は一切扱わず、すべてコードとその構造だけを掘り下げます。
Jon が正しいスタートを切る
The Witness のビルトインエディタには「Movement Panel」という UI コンポーネントがあります。これは「rotate 90°」などの操作を行うボタンが並ぶフローティングウィンドウです。当初は小さく、数個のボタンしかありませんでした。しかし私はエディタを拡張する際に多くの機能を追加したため、パネルの内容は大幅に増えました。これには UI 要素を追加する方法を学ぶ必要がありました。
既存コードを見るとこうなっています:
int num_categories = 4; int category_height = ypad + 1.2 * body_font->character_height; float x0 = x; float y0 = y; float title_height = draw_title(x0, y0, title); float height = title_height + num_categories * category_height + ypad; my_height = height; y0 -= title_height; { y0 -= category_height; char *string = "Auto Snap"; bool pressed = draw_big_text_button(x0, y0, my_width, category_height, string); if (pressed) do_auto_snap(this); } { y0 -= category_height; char *string = "Reset Orientation"; bool pressed = draw_big_text_button(x0, y0, my_width, category_height, string); if (pressed) { /* … */ } } // …
Jon(オリジナルの開発者)は成功への道筋を非常に分かりやすく作ってくれました。多くの場合、シンプルな機能を書いたコードは不要な構造と間接参照で複雑化していますが、ここでは「タイトルバーを配置し、描画し、その下に Auto Snap ボタンを置き、押されたら自動スナップを実行…」という流れがまるで人に UI パネルを作る手順を書いているかのように明確です。これこそプログラミングの正しい姿です。
ただし、このコードは大量の UI を扱うには不向きです。レイアウト作業がすべて行内でハードコーディングされており、複雑な配置(同じ行に 4 個のボタンなど)になるとさらに煩わしくなります。
{ y0 -= category_height; float w = my_width / 4.0f; // 各ボタンの幅 float x1 = x0 + w; float x2 = x1 + w; float x3 = x2 + w; unsigned long button_color, button_color_bright, text_color; get_button_properties(this, motion_mask_x, &button_color, &button_color_bright, &text_color); bool x_pressed = draw_big_text_button(x0, y0, w, category_height, "X", button_color, button_color_bright, text_color); // … Y, Z, Local の繰り返し … }
そこで私は「コードを簡素化するために基盤となるロジックを抽象化したい」と考えました。何故そう感じたのか? それは「効率的なプログラミング」=「開発者がコーディング・デバッグ・変更・再利用までに必要な人間労力を最小化すること」に対する直感です。
効率性とは何か
コードを書き上げる際の二つの主要タスクは、① 何を処理機が実行すべきか決定し、② それを使用言語で最も効率的に表現することです。後者が多くの場合時間と労力を占めます。
経験豊かなプログラマは「効率」とは単なるパフォーマンスの最適化ではなく、「開発プロセス自体」の最適化であると理解しています。つまり、コードを書き、動かし、修正し、デバッグし、他用途に再利用するまでの総合的な人間労力を削減することです。
この観点から、私が結論付けた最も効率的なプログラミング手法は「コードを辞書圧縮器(コンプレッサー)として扱う」ことです。実際に PKZip のようにコードを圧縮し、意味的に小さくする――重複や類似性を排除して本質のみを残す――というイメージです。
これはボトムアップのアプローチであり、「リファクタリング」と呼ばれることもあります。リファクタリングはコードを整理し、再利用可能にする作業ですが、私が強調したいポイントは「実際に使ってみてから抽象化すべき」という点です。
Compression‑Oriented Programming(圧縮志向プログラミング)の見え方
良いコンプレッサーのように、少なくとも 2 回以上出現したパターンを再利用します。多くの人は「すぐに再利用可能なコードを書く」ことを誤解しがちですが、実際には まず具体的で実行可能なコードを書き、それから重複を見つけて共通化する べきです。
- 1 回だけ書くと、何が必要か把握できない。
- 2 回目以降に共通点を抽出すれば、再利用性の高い構造が自然に浮上します。
また、新しい場所で既存コードを再利用したい場合は「そのまま使う」「修正して使う」「新たなレイヤーを追加する」の三択です。この判断は実際に使ってみての経験から来ます。
最終的に、圧縮されたコードは読みやすく、保守・拡張が容易になります。
Witness UI コードへの Compression
まず、C++ の関数はローカル変数を自己完結させるため、同じ計算を何度も書きたくなります。例えば:
int category_height = ypad + 1.2 * body_font->character_height; float y0 = y; // …y0 -= category_height;… // …y0 -= category_height;…
これらはすべて同じパネル UI の構造です。他のパネルでも同様に開始時・ボタン計算などが繰り返されています。そこで 共通するデータを構造体としてまとめます。
struct Panel_Layout { float width; // 「my_width」からリネーム float row_height; // 「category_height」からリネーム float at_x; // 「x0」からリネーム float at_y; // 「y0」からリネーム };
これを使ってコードを書き直すと:
Panel_Layout layout; int num_categories = 4; layout.row_height = ypad + 1.2 * body_font->character_height; layout.at_x = x; layout.at_y = y; float title_height = draw_title(layout.at_x, layout.at_y, title); float height = title_height + num_categories * layout.row_height + ypad; my_height = height; layout.at_y -= title_height; // …
まだ大きな改善ではありませんが、共通部分を分離した第一歩です。次に 関数化 してさらに簡素化します。
Panel_Layout::Panel_Layout(Panel *panel, float left_x, float top_y, float width) { row_height = panel->ypad + 1.2 * panel->body_font->character_height; at_y = top_y; // 初期 y を記憶 at_x = left_x; } void Panel_Layout::row() { at_y -= row_height; } void Panel_Layout::window_title(char *title) { float title_height = draw_title(at_x, at_y, title); at_y -= title_height; }
こうするとメインコードは:
Panel_Layout layout(this, x, y, my_width); layout.window_title(title); int num_categories = 4; float height = title_height + num_categories * layout.row_height + ypad; my_height = height; { layout.row(); bool pressed = layout.push_button("Auto Snap"); if (pressed) do_auto_snap(this); } { layout.row(); bool pressed = layout.push_button("Reset Orientation"); if (pressed) { /* … */ } } // … layout.complete(this);
さらに
push_button をインライン化します。
bool Panel_Layout::push_button(char *text) { return panel->draw_big_text_button(at_x, at_y, width, row_height, text); }
最終的に得られるコードは以下のようになります:
Panel_Layout layout(this, x, y, my_width); layout.window_title(title); layout.row(); if (layout.push_button("Auto Snap")) do_auto_snap(this); layout.row(); if (layout.push_button("Reset Orientation")) { /* … */ } layout.complete(this);
これで、重複した計算やボイラープレートが完全に排除され、読みやすさと保守性が大幅に向上しました。
まとめ
- まず具体的なコードを書いて動かす
- 重複を見つけたら共通化して圧縮する
- 抽象化は少なくとも二例以上出現した後に行う
- イテレートしながら段階的にコードを簡素化する
この手法は「意味的コンプレッション」によるものです。コードが小さくなると、読みやすさ・保守性・拡張性が飛躍的に向上します。ぜひ実際のプロジェクトで試してみてください。