【実務・中級編】Strict Modeにおける『Shape』の構造的部分型とインターフェースの使い分け – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

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、本当にそのスコープだけで完結しているか?そろそろインターフェースに昇格させるべきフェーズじゃないのか?」

その一言が、チームのコードベースを世界最高峰のレベルへと引き上げる。

タイトルとURLをコピーしました