Hackにおける厳格な静的型付け(Strict Mode)と型チェッカー:Shapeの構造的部分型とインターフェースの真の境界線
コードレビューの場で、こんなコードを見かけたことはない連か?
// どこにでもある、DTOのつもりで書かれた脆弱なコード
async function processUserData(shape(
‘id’ => int,
‘name’ => string,
‘email’ => string,
…
> $user): Awaitable
// 処理…
}
「おっと、動的言語のノリをHackに持ち込むな」と私は即座に赤を入れる。
HHVMの型チェッカー(`hh_client`)は強力だ。だが、その強さを活かすも殺すも、型システムに対するエンジニアの解像度次第なのだ。
今回は、Hackの`strict`モードにおけるShapeの構造的部分型(Structural Subtyping)とインターフェース(Nominal Subtyping:公称型)の境界線を、HHVMのメモリモデルと型推論の裏側まで踏み込んで徹底的に解説する。データ転送オブジェクト(DTO)設計の最適解をここで掴んでほしい。
—
1. Shapeの構造的部分型 vs インターフェースの公称型
まず、大前提を整理する。
Hackのクラスやインターフェースは公称的型付け(Nominal Typing)だ。つまり、どれだけプロパティが一致していきようとも、明示的に `implements` していなければ型エラーになる。
対して、`shape` は本質的に構造的型付け(Structural Typing)の側面を持つ。しかし、完全な構造的型付けかと言えば、型チェッカーのルールは厳格だ。
Shapeの「部分型(Subtyping)」のルール
HackのShapeには、オプショナルキー(`?`)や、非密封型(Unsealed Shape)の概念が存在する。
- 密封型(Sealed Shape): 定義されたキー以外の存在を許さない。
- 非密封型(Unsealed Shape / `…`): 定義されていない追加のキーが存在することを許容する。
ここで多くのエンジニアが犯す最大の過ちは、「DTOだからとりあえずShapeでいいや」と巨大なShapeを作り、至る所でそれを使い回すことだ。これはHHVMの型チェッカーにとっても、保守性の観点からも悪手である。
—
2. なぜ巨大なShapeの使い回しは「悪手」なのか?
1. 型チェッカーの負荷とエラーメッセージの肥大化:
ネストした巨大なShape同士を比較する際、型チェッカーはすべてのキーとバリューの型を再帰的に検証する。コンパイルキャッシュが効かない複雑な構造は、CIの速度を確実に殺す。
2. 意図しないプロパティの伝播:
構造的部分型ゆえに、「関数が要求する以上のフィールド」を持つShapeを渡せてしまう。これがドメインロジックの意図せぬ結合を生む。
では、どのような基準でShapeとインターフェースを使い分けるべきか?
- Shapeを使うべき場面: APIのペイロードのパース直後、ローカルスコープ内での一時的なデータ構造、完全に閉じたトランスフォーム処理。
- インターフェース(Class)を使うべき場面: ドメインモデル、振る舞い(メソッド)を伴うオブジェクト、システム間で永続的に契約されるDTO。
—
3. 実践:プロダクションコードで示す最適解
外部APIから非同期でユーザーデータと権限データを受け取り、それを安全に処理するパイプラインを例に取ろう。
ここでは、「境界(Boundary)ではShapeで安全に受取り、ドメイン層ではインターフェースに昇華させる」という鉄則のパターンを示す。
namespace App\DTO;
<<__EnforceStrict>>
module UserDomain;
/
- 外部APIからの生のレスポンスを安全にキャッチするためのShape定義。
- 未知のフィールドが含まれる可能性を考慮し、非密封型(…)を採用する。
/
type RawUserApiResponse = shape(
‘id’ => int,
‘username’ => string,
‘email’ => string,
‘metadata’ => ?shape(
‘last_login’ => string,
…
),
…
);
/
- ドメイン層で厳格に振る舞いを規定するインターフェース(公称型)
/
interface IUserDTO {
public function getId(): int;
public function getDisplayName(): string;
public function getEmail(): string;
}
/
- インターフェースを実装したイミュータブルなDTOクラス
/
final class UserDTO implements IUserDTO {
public function __construct(
private int $id,
private string $username,
private string $email,
) {}
public function getId(): int {
return $this->id;
}
public function getDisplayName(): string {
// ドメイン固有のフォーマットロジックなど
return $this->username;
}
public function getEmail(): string {
return $this->email;
}
}
final class UserTransformer {
/
- Shape(構造的)からインターフェース(公称的)への変換。
- ここがシステム全体の「型安全の要(要塞)」となる。
/
public static function fromShape(RawUserApiResponse $raw): IUserDTO {
// Hackの型ガードとランタイム検査を活用した堅牢なマッピング
$id = $raw[‘id’];
$username = $raw[‘username’];
$email = $raw[‘email’];
return new UserDTO($id, $username, $email);
}
}
/
- 非同期API連携のシミュレーション
/
async function fetchUserDataFromApiAsync(int $userId): Awaitable
// 実際にはここで外部HTTPクライアントを叩く
await Concurrent\sleep(milliseconds: 10);
return shape(
‘id’ => $userId,
‘username’ => ‘architect_hack’,
‘email’ => ‘architect@example.com’,
‘metadata’ => shape(‘last_login’ => ‘202X-01-01T00:00:00Z’),
‘extra_field_from_third_party’ => ‘ignored_safely’,
);
}
/
- エントリーポイント:ドメイン層のインターフェースに依存し、具象に依存しない美しいコード
/
async function handleUserRequestAsync(int $userId): Awaitable
// 1. 境界でShapeとして受け取る
$rawResponse = await fetchUserDataFromApiAsync($userId);
// 2. 即座にドメインモデル(インターフェース)へ変換
$userDto = UserTransformer::fromShape($rawResponse);
// 3. 以降のビジネスロジックはすべてインターフェースを通じて行われる
renderUserProfile($userDto);
}
function renderUserProfile(IUserDTO $user): void {
echo “User: ” . $user->getDisplayName() . ” (” . $user->getEmail() . “)\n”;
}
この設計がプロダクションで最強である理由
1. 外部変更に対する耐性 (Resilience to External Changes):
外部APIが勝手に新しいフィールドを追加してきても、`RawUserApiResponse` が非密封(`…`)であれば型エラーにならない。壊れやすい境界線を最小限に抑え込んでいる。
2. ドメイン層の純粋性 (Purity of Domain):
ビジネスロジック層に配列やShapeが漏れ出さない。すべて `IUserDTO` というインターフェースに抽象化されているため、モックの差し替えや単体テストが極めて容易になる。
3. HHVMの最適化恩恵:
HHVMはクラスベースのオブジェクト(特に `final class`)に対して強力なJITコンパイル時の最適化(プロパティのインライン展開や脱仮想化)を適用する。巨大なShapeをメソッド間でたらい回すよりも、適切なライフサイクルでクラスインスタンスに昇華させた方が、長期的には実行時パフォーマンス的にも有利に働くケースが多い。
—
チーフアーキテクトからの提言
「型を合わせるだけのプログラミング」は今日で終わりにしろ。
Hackの厳格モード(`<<__EnforceStrict>>`)の恩恵を最大限に引き出すのは、データ構造の「形状(Shape)」に依存してコードを散らかすことではなく、「境界で構造を受け止め、コアで概念(インターフェース)を支配する」というアーキテクチャの共通認識だ。
コードレビューで巨大なShapeを見つけたら、こう問うてほしい。
「おい、このShape、本当にそのスコープだけで完結しているか?そろそろインターフェースに昇格させるべきフェーズじゃないのか?」
その一言が、チームのコードベースを世界最高峰のレベルへと引き上げる。