Hackの構造的型付けと公称型:設計の「境界」を支配するアーキテクトの思考法
Hackのコードベースで最も頻繁に目にする「設計の迷い」がある。それは、「このデータ構造をShapeで受けるべきか、それともInterfaceを定義すべきか」という問いだ。
PHPのレガシーを引きずったエンジニアほど、すべてのデータ構造をクラスに閉じ込めようとする。あるいは、型定義をサボるために安易にShapeを多用し、後から「このShape、どこで定義されたんだ?」というデバッグ地獄に陥る。
今日は、HHVMの型チェッカー(HackC)の挙動と、メモリ消費、そしてコードの生存期間を見据えた「設計の極意」を授ける。
—
1. 構造的部分型(Shape) vs 公称的部分型(Interface)
まず、脳のOSを書き換えろ。
- Shape (`shape(…)`): データそのもの。「何を持っているか(構造)」に価値がある。
- Interface (`interface`): 振る舞い。「何ができるか(能力)」に価値がある。
この境界線を曖昧にすると、コードは腐敗する。
Shapeを選択すべき「静的」な瞬間
Shapeは、APIのレスポンスや、永続化層からのスナップショットのように「振る舞いを持たないデータ」を扱うためにある。これは構造的部分型であり、特定の型名に依存せずに型チェックが通るため、疎結合な境界で威力を発揮する。
Interfaceを選択すべき「動的」な瞬間
クラスインターフェースは、そのデータに対して「操作」が必要なときに使う。例えば、`format()`メソッドが必要だったり、実行時にポリモーフィズムが必要な場合は、絶対にShapeを使ってはならない。Shapeでこれを解決しようとすると、`match`式で型ガードを散りばめる羽目になり、拡張性が死ぬ。
—
2. 実務で「負債」を生むアンチパターン
多くのエンジニアが犯すミスは、「Shapeにロジックを詰め込む」ことだ。
// 悪い例:Shapeに対して無理やりロジックを結合させようとする
type TUser = shape(‘id’ => int, ‘name’ => string);
function getDisplayName(TUser $user): string {
// ロジックが散らばり、変更に弱くなる
return $user[‘name’] . ‘ (ID: ‘ . (string)$user[‘id’] . ‘)’;
}
このコードの問題点は、`TUser`というデータ構造が変わった瞬間に、プロジェクト中のあらゆる関数を修正しなければならないことだ。これを解決するのが、「値オブジェクト(Value Object)」としてのクラス設計である。
—
3. 生産性を最大化する「堅牢な設計」パターン
では、どう設計すればいいのか。私が推奨する黄金パターンは、「受信時はShape、処理時はドメインクラス」という変換レイヤーを設けることだ。
namespace App\Domain;
// 1. 外部境界(API連携等)では高速なShapeを利用
type TUserPayload = shape(‘id’ => int, ‘name’ => string);
// 2. 内部処理では、カプセル化されたクラスを利用
final class User {
public function __construct(
private int $id,
private string $name,
) {}
public function getFormattedName(): string {
return “{$this->name} (ID: {$this->id})”;
}
// 変換の責務をクラス自身に持たせる
public static function fromShape(TUserPayload $payload): this {
return new self($payload[‘id’], $payload[‘name’]);
}
}
なぜこの設計が最強なのか?
1. 境界の隔離: APIのスキーマが変わっても、`fromShape`メソッドを一箇所修正するだけで、ビジネスロジックへの影響をゼロにできる。
2. 型チェッカーの最適化: HHVMはクラス内のプロパティアクセスを高度に最適化する。Shapeのキーアクセスは連想配列のルックアップコストがかかるが、プロパティアクセスはインライン化の恩恵を受けやすい。
3. 可読性: `User::getFormattedName()`を呼ぶ方が、`$user[‘name’]`を直接操作するよりも意図が明確だ。
—
4. チーフアーキテクトからの「最後のアドバイス」
設計時に迷ったら、以下の「3秒判断」を使ってほしい。
- そのデータにメソッドを生やしたいか?
- Yes → `class` (interface) を定義せよ。
- No → `shape` で十分だ。
- そのデータは型名として再利用するか?
- Yes → `newtype` や `type` エイリアスで名前をつけろ。
- No → インラインで書け。
Hackは「厳格さ」を武器にする言語だ。動的言語のような柔軟性を求めるのではなく、「型がコードを語る」状態をいかに作るか。それが、HHVM上で最高のパフォーマンスと保守性を引き出す唯一のルートだ。
さあ、コードレビューに戻れ。次は、その「曖昧なShape」がクラスにリファクタリングされていることを期待している。