【実務・中級編】Hackにおける『Variance(共変・反変)』の理解:ジェネリクスを安全に扱うための理論と実践 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

Hackを掌握する極限の知見:ジェネリクスの変性(Variance)と静的型安全性の極致

コードレビューの現場で、次のようなジェネリクスの型エラーに直面して頭を抱えたことはないだろうか。

> 「Expected `MyInterface`, but got `MyInterface` instead.」

「子クラスのインスタンスを渡しているのだから、親クラスを期待する場所でそのまま動くべきではないのか?」——この疑問に即答できないうちは、どれほどモダンなPHP/Hackのコードを書いたとしても、型システムの奥底を掌握しているとは言えない。

HHVMの静的型チェッカー(`hh_client`)は、妥協のない厳格さでコードの安全性を担保している。その堅牢性の要(かなめ)となるのが、ジェネリクスにおける変性(Variance:共変・反変・非変)の概念だ。

今回は、HackのStrictモード(`1. 変性(Variance)とは何か:HHVM型チェッカーの視点

ジェネリクス型 `I` において、型パラメータ `T` の親子関係が、そのままコンテナ型 `I` 全体の親子関係にどう影響するかを示す性質を変性と呼ぶ。

Hack(および多くの静的言語)では、デフォルトで非変(Invariant)である。つまり、`Dog` が `Animal` のサブタイプであっても、`Box` は `Box` のサブタイプではない。これには明確な理由がある。もし非変性を破ると、型安全性が根底から崩壊するからだ。

非変性がもたらす安全性の壁

{
private ?T $item = null;

public function set(T $item): void {
$this->item = $item;
}

public function get(): ?T {
$this->item;
}
}

もし仮に `Box` が `Box` に代入可能(共変)だとすると、次のようなコードが通ってしまう。

// 擬似的な危険コードのイメージ
$dogBox = new Box();
Box $animalBox = $dogBox; // 共変だと仮定

// Animalの箱なのだから、Catを入れてもよさそうに見える
$animalBox->set(new Cat());

// 元の$dogBoxの中身を取り出すと…Catが取り出され、Dogとして扱われてクラッシュ!
$dogBox->get()->bark();

この破綻を防ぐため、Hackのデフォルトは非変なのだ。しかし、「読み取り専用(Producer)」や「書き込み専用(Consumer)」の文脈においては、この制約を安全に緩める必要がある。それが共変(Covariance: `+T`)と反変(Contravariance: `-T`)である。

—

2. 共変(Covariance: `+T`)と反変(Contravariance: `-T`)のルール

Hackでは、インターフェイスやエイリアスの型パラメータに対して明示的に変性を指定できる。

1. 共変(`+T`): サブタイプの方向を維持する。出力(Producer)側にのみ `T` を使える。
2. 反変(`-T`): サブタイプの方向を反転させる。入力(Consumer)側にのみ `T` を使える。

Liskovの置換原則に基づき、型チェッカーは「メソッドの引数は反変、戻り値は共変」という鉄則を検証している。

—

3. 【プロダクションコード設計】非同期API・リポジトリ層での実戦パターンの構築

実際のWebアプリケーション開発において、変性が最も活きる領域は、データプロバイダ(読み取り系)とハンドラ(書き込み・処理系)の分離設計だ。

以下に、厳格なStrictモードで構築された、ドメインモデルの読み書きを安全かつエレガントに抽象化するプロダクションコードを示す。

は IReadOnlyProvider が期待される場所に安全に渡せる。
interface IReadOnlyProvider<+T as Model> {
public function fetchById(int $id): ?T;
}

// — 2. 反変(Contravariance: -T)を活用した「コンシューマ/シリアライザ」 —
// データを「消費する(Consume)」だけのインターフェイスには ‘-‘ を付与する。
// これにより、IModelConsumer は IModelConsumer が期待される場所に渡せる。
interface IModelConsumer<-T as Model> {
public function process(T $model): void;
}

// — 3. 実装クラス群 —

class UserRepository implements IReadOnlyProvider {
public function fetchById(int $id): ?User {
// DBからユーザーを取得するロジック(ダミー)
return new User(“Alice”);
}
}

class AdminAuditLogger implements IModelConsumer {
public function process(AdminUser $model): void {
\printf(“Logging Admin action for: %s\n”, $model->name);
}
}

// — 4. 実際のサービス層での利用 —

class ApplicationService {

// 読み取り側:共変のおかげで、より狭い範囲(AdminUser)を返すプロバイダも、
// Userを期待するこのメソッドへシームレスに注入できる。
public static function printUserName(IReadOnlyProvider $provider, int $id): void {
$user = $provider->fetchById($id);
if ($user !== null) {
echo “UserName: ” . $user->name . “\n”;
}
}

// 書き込み側:反変のおかげで、一般的なUserを受け入れるロガーを、
// AdminUser専用の処理パイプラインに流し込むことができる。
public static function executeAudit(IModelConsumer $consumer, AdminUser $admin): void {
$consumer->process($admin);
}
}

この設計がもたらす圧倒的なメリット

  • 拡張性と再利用性の爆発的向上: `IReadOnlyProvider<+T>` に共変性を適用しているため、`UserRepository`(`User`を返す)だけでなく、将来的に追加される `AdminUserRepository`(`AdminUser`を返す)を、既存のアプリケーション層のコードを1行も書き換えることなく差し替え可能になる。
  • コンパイル時の完全な安全性: もし共変インターフェイスのメソッドの引数に `T` を使おうものなら、HHVMの型チェッカーが即座にエラーを吐き、実行時エラーの芽を摘み取る。

—

4. チーフアーキテクトからの実践的アドバイス:ありがちなアンチパターン

コードレビューでよく見かけるミスと、その対処法をまとめる。

❌ やってはいけないこと:変性の乱用とミュータブルな状態の混在

状態を持つ(Mutableな)クラスの型パラメータに変性を付与しようとしてはならない。変性は本質的にイミュータブル(不変)な構造、あるいは特定の方向(入出力)に限定されたインターフェイスでのみ真価を発揮する。

💡 パフォーマンス上の注意点

HHVMのJITコンパイラは非常に高度であり、厳格な静的型付けが施されたコードほど、最適化の恩恵を受けてネイティブマシン語へのコンパイル効率が跳ね上がる。変性を正しく利用してインターフェイスを細粒度に分離することは、保守性だけでなく、実行時オーバーヘッドの削減(ボックス化の回避など)にも直結する。

—

総括

Hackにおける変性(Variance)は、単なるアカデミックな型理論の遊戯ではない。大規模なWebアプリケーションのコードベースにおいて、モジュール間の結合度を下げ、変更に強く、かつ「絶対に実行時型エラーを起こさない」という絶対的な保証を手に入れるための最強の武器である。

型チェッカーと対話し、共変と反変の境界線を完璧にコントロールできた時、あなたの書くコードは単なる「動くプログラム」から、芸術的なまでに堅牢な「エンジニアリングの結晶」へと昇華するはずだ。

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