Hackの『Shapes』を掌握する:構造的部分型の罠とドメインモデリングの境界線
Hackの型システムを使いこなす上で、多くのエンジニアが「なんとなく」で使い分けているのが `shape` 型と `class`(名前付き型)だ。
「とりあえずデータ構造を定義したいから `shape` でいいか」――この判断が、数ヶ月後のコードベースに致命的な技術的負債を刻む。HHVMのチーフアーキテクトとして断言しよう。`shape` は「匿名データの搬送手段」であり、`class` は「ドメインの境界線を守る盾」だ。 この二つを混同した設計は、型チェックの恩恵を自らドブに捨てるに等しい。
今日は、Hackにおける構造的部分型(Shapes)と名前付き型の正しい使い分け、そして実務で「死なないコード」を書くための設計指針を授ける。
—
1. Shapesの本質:構造的という名の「諸刃の剣」
Hackの `shape` は構造的部分型(Structural Subtyping)に基づいている。これは「中身が同じ形をしていれば、それは同じものとみなす」という強力な柔軟性を提供する。
なぜこれが危険なのか?
APIのレスポンスやDTO(Data Transfer Object)の定義において、`shape` は便利だ。しかし、`shape` にビジネスロジックを寄せていくと、型チェッカーは「意図」を理解できなくなる。
// 悪い例:ドメインロジックがShapeに依存している
type UserProfile = shape(
‘id’ => int,
‘email’ => string,
);
function sendEmail(UserProfile $user): void {
// ここで渡されるのが「本当にUserなのか」を型チェッカーは厳格に検証しない
// 同じフィールドを持つ別のshapeが来ても通ってしまう
}
構造的部分型は「名前」ではなく「形状」で判定するため、似たような `shape` を定義した瞬間に、型システムによる保護境界が崩壊する。これを防ぐには、「外部との境界線」以外では `shape` を使わないという鉄則を守るべきだ。
—
2. 名前付き型によるドメインモデリング:堅牢さの正体
一方で、`class` や `record` を使った名前付き型は、型システムに「アイデンティティ」を与える。たとえ内部構造が同じ `(int, string)` であったとしても、`User` クラスと `Admin` クラスは別物として扱われる。
推奨される設計パターン:境界での変換(Anti-Corruption Layer)
外部APIから受け取った `shape` をそのままビジネスロジックに流し込んではいけない。アプリケーションの境界で、必ず「値オブジェクト」や「ドメインエンティティ」へ昇格させる。
// 境界線:ここまではShapeで受け取って良い
type RawUserResponse = shape(
‘id’ => int,
‘email’ => string,
);
// ドメインモデル:ロジックはここで行う
final class User {
public function __construct(
private int $id,
private string $email,
) {}
public static function fromShape(RawUserResponse $data): this {
// ここでバリデーションを完結させる
if ($data[‘id’] <= 0) throw new InvalidArgumentException("ID不正");
return new self($data['id'], $data['email']);
}
}
この設計により、アプリケーションの深部では `User` オブジェクトだけを扱い、型安全性が完全に担保される。`shape` はあくまで「外部との通信プロトコル」としてのみ機能させるのだ。
---
3. パフォーマンスとHHVMの最適化
HHVMにおいて、`shape` は配列の派生として最適化されているが、複雑な `shape` を多用すると、JITコンパイラの型推論負荷が増大する可能性がある。
- `shape` のコピーコスト: `shape` は不変(immutable)の振る舞いをする際、内部的なコピーが発生することがある。数万件のデータを扱うバッチ処理では、`shape` ではなく `class` を用いて参照渡しを意識した設計にするほうが、メモリ効率とGCの負荷軽減において有利に働く。
- 型チェッカーのオーバーヘッド: `shape` が複雑にネストすると、Hackの型チェッカーはユニフィケーション(型結合)に時間を費やす。型定義はなるべく浅く保て。
—
4. チーフアーキテクトからの提言:判断基準
システム設計時に迷ったら、以下の基準で即断せよ。
1. その型は「データ」か、「振る舞い」か?
- データ(JSON、APIレスポンス、DBレコード)なら `shape` を検討してよい。
- 振る舞い(メソッド、状態管理、バリデーション)が必要なら、迷わず `class` を書け。
2. その型は「公開」されるか?
- パッケージの外へ公開するなら、型安全性を保証するために名前付き型(interface含む)を要求しろ。`shape` を型として公開するのは、インターフェースを定義する努力を放棄しているのと同じだ。
3. チームが「型」を誤認するリスクはあるか?
- `shape` には型名が存在しないため、エラーメッセージが常に `’shape(…)’` と表示され、デバッグが困難になる。コードの意図を明確にするためにも、特定の意味を持つ構造体には必ず名前を付けろ。
結論
Hackの `shape` は「手軽な道具」だが、それは「使い捨ての道具」であることを忘れてはならない。長期的な保守性を求めるのであれば、型に名前を与え、ドメインの境界を定義する。 これこそが、大規模システムを破綻させない唯一の道だ。
コードは、ただ動けばいいのではない。「誰が読んでも型チェッカーの意図が伝わる」ように書くこと。それこそが、Hackを掌握するエンジニアの矜持だ。