Shape型の魔力と罠:構造的部分型を制する者がHackの型システムを制する
コードレビューをしていて、次のようなコードに出くくしたことはないだろうか。
<<__Strict>>
namespace HackExpert\Blog;
type UserShape = shape(‘id’ => int, ‘name’ => string, ‘email’ => string);
function process_user(UserShape $user): void {
// 処理…
}
一見して何の問題もない、美しく厳格なHackのコードに見える。しかし、チーフアーキテクトの視点から言わせてもらえば、このコードは「Shape型の構造的部分型がもたらす暗黙の結合」という地雷を踏む前兆に満ちている。
今回は、Hackの`shape`が持つ圧倒的な柔軟性の裏に隠された型安全性の落とし穴を暴き、HHVMの型チェッカー(hhvm)を味方につけて堅牢なドメインモデルを構築するための実践的戦略を伝授しよう。
—
1. Shape型の「構造的部分型」が引き起こすサイレントバグ
まず前提として、Hackの`shape`は名前付きの公称型(Nominal Type)ではなく、構造的部分型(Structural Subtyping)に基づいている。つまり、型チェッカーは「データの構造(キーと値の型)」だけで互換性を判断する。
ここで、以下のコードを見てほしい。
<<__Strict>>
namespace HackExpert\Blog;
type Point2D = shape(‘x’ => float, ‘y’ => float);
type Point3D = shape(‘x’ => float, ‘y’ => float, ‘z’ => float);
function render_2d(Point2D $p): void {
// 描画処理
}
function calculate_vector(Point3D $p3): void {
// Point3Dを受け取る関数だが、もしここにPoint2Dを渡したらどうなるか?
// 逆に、Point3DはPoint2Dが要求する構造をすべて満たしている(余分なキーを持つ)。
}
Hackにおいて、より多くのフィールドを持つShape(`Point3D`)は、より少ないフィールドを要求するShape(`Point2D`)の部分型(subtype)として扱われる。そのため、`Point2D`を要求する関数に`Point3D`を渡すことは静的に安全に許可される。
問題は、その逆や、APIレスポンスなどの外部境界だ。次のようなケースを考えてみてほしい。
<<__Strict>>
namespace HackExpert\Blog;
type APIResponse = shape(‘status’ => int, ‘data’ => shape(‘id’ => int));
function handle_response(shape(‘status’ => int) $res): void {
// status だけ見て安心しているが、data の構造が想定外に変化した場合、
// 実行時あるいは別のレイヤーで致命的なバグを生む。
}
Shapeの構造的柔軟性は、リファクタリング時に「どこが暗黙的に依存しているか」を追跡困難にする。IDEの「参照を検索(Find Usages)」が機能しにくくなり、気づけばコードベース全体にゾンビのようなデータ構造が蔓延することになるのだ。
—
2. いつShapeを使い、いつ「名前付きクラス(Class)」を使うべきか
実務の現場において、このジレンマに対する明確な境界線を引く必要がある。
- Shapeを使うべき場面
- 関数のプライベートな戻り値のタプル的利用や、一時的なデータのグループ化。
- 厳密に単一のモジュール内・ファイル内で完結するデータ構造。
- JSONのデコード結果など、外部入力を最初に受け取る「アンチ腐敗層(Anti-Corruption Layer)」の入り口。
- クラス(`class` / `record`)を使うべき場面
- ビジネスロジック(振る舞い)を持つドメインモデル。
- システム全体で共有されるエンティティや値オブジェクト(Value Object)。
- 他のコンポーネントのパブリックAPIのシグネチャに登場するデータ構造。
「ただのデータの入れ物だから」という理由で安易にグローバルな`type`としてShapeを定義するのは、保守性を自ら捨てる行為に等しい。
—
3. 【プロダクションコード例】堅牢性と型安全性を極めた設計パターン
では、外部API連携や複雑なコンポーネント間通信において、Shapeの柔軟性を安全に活かしつつ、バグを完全に排除する設計とはどのようなものか。
以下の「外部APIからユーザー情報を取得し、厳格なバリデーションを経てドメインモデルへ昇華させる」プロダクションコードを見てほしい。コピペしてそのまま現場の設計指針として応用してほしい。
<<__Strict>>
namespace HackExpert\Blog\Production;
/
- 【アンチ腐敗層】
- 外部APIのレスポンスを表現するShape。これは外部の都合に依存するため、
- アプリケーションの奥深くへそのまま伝播させてはならない。
/
type RawUserApiResponse = shape(
‘id’ => int,
‘username’ => string,
‘email’ => string,
?string ‘optional_bio’ => ?string,
);
/
- 【ドメインモデル】
- ビジネスロジックをカプセル化するクラス。公称型(Nominal Type)として機能し、
- 型チェッカーと開発者の意図を強く結びつける。
/
final class UserProfile {
public function __init__(
private int $id,
private string $username,
private string $email,
private ?string $bio,
) {}
public function getId(): int {
return $this->id;
}
public function getUsername(): string {
return $username;
}
public function getEmail(): string {
return $this->email;
}
public function getBio(): ?string {
return $this->bio;
}
}
/
- 【境界防衛とマッピング】
- Shapeで受け取った構造体を、厳格な型を持つクラスインスタンスへ変換する。
- ここで構造的部分型の曖昧さを断ち切る。
/
final class UserMapper {
public static function fromApiResponse(RawUserApiResponse $raw): UserProfile {
// HHVMの静的解析と実行時チェックの境界
// 必要に応じてここで追加のバリデーションを実行する
$id = $raw[‘id’];
if ($id <= 0) {
throw new \InvalidArgumentException('Invalid User ID detected from API.');
}
return new UserProfile(
$id,
$raw['username'],
$raw['email'],
Shapes::idx($raw, 'optional_bio'), // 安全なオプショナルキーの取得
);
}
}
/
- 【クライアントコード】
- 外部APIを叩いてドメインモデルを生成し、安全に処理を行う例
/
async function fetch_and_process_user_async(int $userId): Awaitable
// 擬似的な外部APIコール
// $rawResponse = await ExternalApiClient::getAsync($userId);
// 型キャストの代わりにMapperを通すことで、構造的型から公称型へ安全に移行
// 開発者は以降、RawUserApiResponseの構造変化に怯える必要がなくなる
// $profile = UserMapper::fromApiResponse($rawResponse);
// 業務ロジックの実行…
}
このコードの優れたポイント
1. 境界の分離(Boundary Isolation): 変動しやすい外部構造(`RawUserApiResponse`)をShapeで局所化し、アプリケーション内部には一切漏れ出させない。
2. `Shapes::idx`の活用: オプショナルなキーにアクセスする際、安全なビルトイン関数を用いることで、キーの存在欠落による実行時エラー(Undefined index)を完全に封じ込めている。
3. 公称型への昇華: 最終的に`UserProfile`という`final class`に変換することで、コードベース全体の意図が明確になり、IDEの支援能力が最大化される。
—
4. チーフアーキテクトからの提言
Hack言語の最大の武器は、厳格な静的型チェックとHHVMの圧倒的な実行パフォーマンスの融合にある。その恩恵を最大限に受けるために、「型に物語を持たせる」ことを忘れないでほしい。
Shape型は非常に強力なナイフだ。しかし、それをむき出しのままアプリケーション全体で振り回せば、やがてコードベースは血まみれになる。
「データ構造の定義にはShapeを、システムの契約(Contract)と振る舞いにはクラスを」――この原則をコードレビューの共通言語としてチームに定着させれば、君たちのプロダクトの堅牢性は次のステージへと確実に飛躍するだろう。