【実務・中級編】Hackの『Contravariance』と『Covariance』:ジェネリクスにおける型安全な継承のルール – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

Hackの変位指定(Variance)を完全掌握せよ:共変と反変がもたらす堅牢な型安全設計

コードレビューをしていて、最も頭痛がする瞬間の一つがこれだ。

「なぜ、`IProducer` を期待しているメソッドに `IProducer` を渡せないんだ?」
「いや、逆に `IConsumer` を期待している場所に `IConsumer` を突っ込もうとするな!」

Hack言語(そしてHHVMの厳格な型チェッカー)は、甘えを許さない。`<<__STRICT__>>` の世界において、ジェネリクス(Generics)とサブタイピング(Subtyping)の噛み合わせを誤ると、型チェッカーは容赦なくビルドを止める。

しかし、この制約は単なる邪魔者ではない。HHVMのJITコンパイラが最高効率でネイティブコードを生成し、数百万リクエストを捌くバックエンドの堅牢性を担保するための「防壁」なのだ。

今回は、Hackにおける共変(Covariance: `+`)と反変(Contravariance: `-`)の本質を、HHVMのアーキテクチャと型チェッカーの挙動を踏まえて徹底的に叩き込む。実務の現場で即座に使える、極限まで洗練されたデザインパターンを見ていこう。

—

1. なぜジェネリクスはデフォルトで「非変(Invariant)」なのか?

まず前提を揃える。Hackにおいて、次のようなインターフェースを定義したとする。

<<__STRICT__>>

interface IContainer {
public function set(T $item) : void;
public function get() : T;
}

この `IContainer` は 非変(Invariant) である。つまり、`T` がどんなに親子関係(`Dog extends Animal`)であっても、`IContainer` と `IContainer` には一切の互換性がない。

なぜか? `set(T $item)` で書き込み(入力)を行い、`get() : T` で読み込み(出力)の両方を行っているからだ。
もし `IContainer` を要求する場所に `IContainer` を代入できてしまったらどうなるか? 穴埋めとして `Animal`(例えば `Cat`)を `set()`できてしまい、後から `get()` した側が `Dog` を期待して型安全性が崩壊する。

型チェッカーは、この「読み書きのジレンマ」を防ぐためにデフォルトで非変にしている。

だが、現実の設計では「読み取り専用だから親子関係をそのままスライドさせたい」「書き込み専用だから逆に扱いたい」というケースが多々ある。そこで登場するのが、変位指定(Variance Annotations)だ。

—

2. 共変(Covariance: `+T`):読み取り専用の世界

概念とルール

  • 記号: `+` (例: `<<__EnforceGenerics__>>` や型パラメータのプレフィックス)
  • 意味: 型の階層構造をそのまま維持する(`T` から `Subtype` の方向へ代入可能)。
  • 制約: 共変型パラメータは、メソッドの「戻り値(出力)」としてのみ使用可能。引数(入力)に使用すると型チェッカーがエラーを吐く。

プロダクション設計パターン:イミュータブルなデータプロバイダ

APIレスポンスやキャッシュ層からデータを読み出すだけのコンポーネントを設計する場合、共変が不可欠となる。

<<__STRICT__>>

abstract class Animal {}
class Dog extends Animal {
public function bark() : string { return “Woof!”; }
}
class Cat extends Animal {
public function meow() : string { return “Meow!”; }
}

/

  • 共変インターフェース
  • +T により、IReadOnlyProvider は IReadOnlyProvider のサブタイプとなる

/
interface IReadOnlyProvider<<__EnforceGenerics__>> +T {
// 戻り値(出力)としての使用はOK
public function provide() : T;

// ❌ NG例: 引数に T を使うと型チェッカーがコンパイルエラーを起こす
// public function consume(T $item) : void;
}

class DogProvider implements IReadOnlyProvider {
public function provide() : Dog {
return new Dog();
}
}

class AnimalClient {
// IReadOnlyProvider を受け取るが、DogProvider も渡せる!
public static function handle(IReadOnlyProvider $provider) : void {
$animal = $provider->provide();
// 確実に Animal として安全に扱える
}
}

// 実行例
<<__EntryPoint>>
function main() : void {
$dogProvider = new DogProvider();
// 共変性のおかげで、IReadOnlyProvider を IReadOnlyProvider として渡せる
AnimalClient::handle($dogProvider);
echo “Covariance check passed.\n”;
}

この設計により、アダプター層やモック層で具体的な子孫クラスのプロバイダを、汎用的な上位コンポーネントに一切のキャストなしで注入できる。

—

3. 反変(Contravariance: `-T`):書き込み・処理専用の世界

概念とルール

  • 記号: `-`
  • 意味: 型の階層構造を逆転させる(`Animal` を受け取れるものは、より特化した `Dog` を受け取る場所にも代入できる)。
  • 制約: 反変型パラメータは、メソッドの「引数(入力)」としてのみ使用可能。

プロダクション設計パターン:ログシンクとイベントハンドラ

「何かのデータを受け取って処理する」コンポーネント(Consumer, Logger, Validatorなど)は、反変のマスターピースだ。

<<__STRICT__>>

/

  • 反変インターフェース
  • -T により、IConsumer は IConsumer の代わりとして使用可能になる

/
interface IConsumer<<__EnforceGenerics__>> -T {
// 引数(入力)としての使用はOK
public function consume(T $item) : void;

// ❌ NG例: 戻り値に T を使うことはできない
// public function produce() : T;
}

class AnimalLogger implements IConsumer {
public function consume(Animal $animal) : void {
// すべての動物のログを取る汎用ロガー
echo “Logging generic animal.\n”;
}
}

class DogTrainer {
// Dog 専用の処理を行うが、内部で「Animal全般を扱えるロガー」を利用したい
public function processDog(Dog $dog, IConsumer $logger) : void {
$logger->consume($dog);
}
}

// 実行例
<<__EntryPoint>>
function main_contravariance() : void {
$animalLogger = new AnimalLogger(); // IConsumer
$trainer = new DogTrainer();

// 反変性により、IConsumer は IConsumer が要求される場所に渡せる!
// 「動物全般を扱えるロガー」は、「犬を扱えるロガー」としても当然機能するからだ。
$trainer->processDog(new Dog(), $animalLogger);
echo “Contravariance check passed.\n”;
}

この反変のルールは、イベント駆動アーキテクチャやミドルウェアのパイプライン設計において、リスナーの型安全性を保ったまま汎用化するために極めて強力な武器となる。

—

4. HHVMアーキテクチャの視点:変位指定がパフォーマンスに与える影響

「なぜこんなに厳格なルールが必要なのか?」
それは、HHVMのJITコンパイラ(Region JIT / Heraclesなど)とタイププロファイリングの挙動に直結しているからだ。

1. ゼロ・オーバーヘッドの型消去(Type Erasure)
Hackのジェネリクスは、JavaのようなType Erasureに近いアプローチをとりつつ、HHVMのVM層で型ヒントが厳密に検証される。変位指定を明示(`+T`, `-T`)することで、型チェッカーはコンパイル時にサブタイピングのグラフを完全解決し、実行時(Runtime)の無駄な `is` チェックやダウンキャストのコストを完全に排除する。
2. メソッドディスパッチの最適化
共変・反変が正しく静的に定義されていると、HHVMはVTable(仮想メソッドテーブル)の構築やインターフェース呼び出しのインライン化を極限まで最適化できる。動的言語的な「実行時エラーの揺らぎ」をコンパイル時に完全に潰し込んでいるため、PHPのダイナミズムを排除した極限のスループットを実現できるのだ。

—

5. チーフアーキテクトからの実践的提言

プロダクションコードで変位指定を設計する際は、以下の原則をチームのコーディング規約として定めてほしい。

1. Producerは「共変(`+T`)」に寄せよ
ファクトリ、プロバイダ、リポジトリの読み取りメソッド、コレクションのイテレータなど、「データを生み出す側」は常に共変の可能性を検討し、上位の抽象型を要求するクライアントに対して高い再利用性を持たせろ。
2. Consumerは「反変(`-T`)」に寄せよ
バリデータ、オブザーバー、ロガー、コールバックハンドラなど、「データを受け取って消費する側」は反変を活用し、汎用的なハンドラを具体的なドメイン層に違和感なく差し込めるように設計せよ。
3. 迷ったら「非変(Invariant)」から始めよ
getter と setter の両方を持つドメインモデルやエンティティを無理に変位させようとしてはいけない。それは設計のバグの元だ。変位指定は「読み取り専用」または「書き込み専用」に特化したインターフェース設計の特権である。

Hackの厳格な型システムと変位指定を味方につけたコードは、もはや単なるスクリプトではない。大規模分散システムを長期間にわたって無停止で支え続ける、美しく堅牢な要塞となる。

次のコードレビューでは、曖昧な型定義をすべて駆逐し、この共変・反変のロジックでコードを極限まで研ぎ澄ましてほしい。

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