【Hack極限知見】共変・反変の完全理解:ジェネリクス型における代入ルールの正体
コードレビューをしていて、次のような型エラーに頭を抱えたことはないか?
> “Expected `MyProducer
> (またはその逆)
「`Dog` は `Animal` のサブタイプなのだから、`MyProducer
この疑問に即答できないうちは、君はまだHackの厳格な型チェッカー(hhvm typechecker)の掌の上で踊らされているにすぎない。
大規模なWebアプリケーション、複雑な非同期APIクライアント、あるいはドメイン駆動設計におけるリポジトリ層を設計する際、ジェネリクスの変性(Variance)を完全に支配できなければ、コードは冗長なボイラープレートの海に沈むか、実行時エラーの爆弾を抱えることになる。
今回は、HHVMの型システムを知り尽くしたチーフアーキテクトの視点から、Hackにおける共変(Covariance)と反変(Contravariance)の正体を暴き、実務で即座に使える堅牢な設計パターンを授けよう。
—
1. なぜ「サブタイプの常識」がジェネリクスで通じないのか?
オブジェクト指向プログラミングにおいて、`Dog` が `Animal` を継承している(`Dog extends Animal`)場合、`Animal` を要求する関数に `Dog` を渡すことは当然できる。これはリスコフの置換原則(LSP)の基本だ。
しかし、これをジェネリクス(総称型)に適用した途端、話が変わる。
Hack(および多くの静的型付け言語)において、デフォルトのジェネリクスは不変(Invariant)である。つまり、`A` と `B` に継承関係があっても、`Container` と `Container` の間には一切の継承関係が発生しない。
なぜか? 理由はシンプルだ。読み込み専用(Producer)か、書き込み専用(Consumer)かによって、安全性(Safety)の方向が真逆になるからだ。
ここで型チェッカーの思考をシミュレートしてみよう。
<<__Strict>>
class Animal {}
class Dog extends Animal {
public function bark(): void { echo “Woof!\n”; }
}
class Box
private ?T $item = null;
public function set(T $item): void { $this->item = $item; }
public function get(): ?T { return $this->item; }
}
もし、`Box
// 仮想的な危険コード
Box
Box
animalBox->set(new Cat()); // Animalの別のサブタイプ「Cat」を挿入!
Dog dog = dogBox->get() as nonnull; // 中身はCatなのにDogとして取り出す… 💥クラッシュ!
型安全神話が崩壊する瞬間だ。したがって、データを書き込む(Consumer)コンポーネントにおいて、型パラメータをそのまま上位型に代入することは型安全性を破壊する。
—
2. 共変(Covariance: `+`)と反変(Contravariance: `-`)の数学的・物理的ルール
Hackでは、ジェネリクス定義時にアノテーションを付与することで、型パラメータの変性を明示的にコントロールできる。
| 変性 | 構文 | 意味 | 許される操作 |
| :— | :— | :— | :— |
| 不変 (Invariant) | `
| 共変 (Covariance) | `<+T>` | サブタイプ方向への代入を許可 | 戻り値(Producer)としてのみ使用可能 |
| 反変 (Contravariance) | `<-T>` | スーパータイプ方向への代入を許可 | 引数(Consumer)としてのみ使用可能 |
共変(`+T`)の本質:生産者(Producer)
「私はデータを取り出すことしかしません(Read-only)」という宣言だ。
外側から見て、より広い型(スーパークラス)を期待する場所に、より狭い型(サブクラス)を返すオブジェクトを差し込むことができる。
反変(`-T`)の本質:消費者(Consumer)
「私はデータを受け取って処理することしかしません(Write-only / Sink)」という宣言だ。
驚くべきことに、反変のシグネチャでは、より狭い型を期待する場所に、より広い型を受け取れるオブジェクトを代入できる。
—
3. 実践!プロダクションコードで見る変性の活用パターン
机上の空論はここまでだ。実際のWebアプリケーション開発(APIレスポンスの処理やイベントハンドリング)で、これらをどう美しく配置すべきかを示そう。
以下のコードは、共変のリーダー(Producer)と反変のシンク(Consumer)を完璧に組み合わせた、そのままプロダクションに投入できる実例だ。
<<__Strict>>
namespace Architecture\VarianceDemo;
// ==========================================
// ドメインモデルの定義
// ==========================================
interface WebEvent {}
class HttpRootEvent implements WebEvent {
public function __construct(public string $uri) {}
}
class ApiPayloadEvent extends HttpRootEvent {
public function __construct(string $uri, public array
parent::__construct($uri);
}
}
// ==========================================
// 1. 共変(Covariant: +T)のイディオム:リーダー/プロデューサー
// ==========================================
// 「T またはそのサブタイプを生産する」ことを保証する。
interface IEventProducer<+T> {
public function produce(): T;
}
class ApiEventProducer implements IEventProducer
public function __construct(private ApiPayloadEvent $event) {}
public function produce(): ApiPayloadEvent {
return $this->event;
}
}
// ==========================================
// 2. 反変(Contravariant: -T)のイディオム:ハンドラ/コンシューマー
// ==========================================
// 「T またはそのスーパータイプを受け入れて処理できる」ことを保証する。
interface IEventHandler<-T> {
public function handle(T $event): void;
}
class HttpRootEventHandler implements IEventHandler
public function handle(HttpRootEvent $event): void {
echo “Handling HTTP Root Event for URI: {$event->uri}\n”;
}
}
// ==========================================
// 3. アーキテクチャ統合:ディスパッチ処理
// ==========================================
class EventDispatcher {
// このメソッドは IEventHandler
public static function registerAndDispatch(
IEventHandler
IEventProducer
): void {
$event = $producer->produce();
// ApiPayloadEvent は HttpRootEvent のサブタイプなので、
// IEventHandler
$handler->handle($event);
}
}
// ==========================================
// 実行エントリポイント
<<__EntryPoint>>
function main(): void {
$producer = new ApiEventProducer(
new ApiPayloadEvent(‘/api/v1/users’, dict[‘status’ => ‘active’])
);
// HttpRootEvent を処理できる汎用ハンドラを用意
$rootHandler = new HttpRootEventHandler();
// 型エラーを起こさず、完璧にバインドされる
EventDispatcher::registerAndDispatch($rootHandler, $producer);
}
このコードの何が美しいのか?
1. `IEventProducer
2. `IEventHandler
3. これにより、フレームワークのミドルウェアやイベントバスの設計において、過剰な型キャスト(`As` 式やインスタンスチェック)を完全に排除し、静的型チェッカーの恩恵を100%受けられる。
—
4. パフォーマンス上の注意点とHHVMの裏側
「共変・反変を使うと、HHVMのJITコンパイルや実行時パフォーマンスにペナルティがあるのではないか?」
優秀なエンジニアなら当然ここに懸念を抱くだろう。結論から言えば、実行時(Runtime)のオーバーヘッドはほぼゼロだ。
- 静的チェックのコスト: 変性の検証はすべてHHVMの静的型チェッカー(`hh_client`)のコンパイル時フェーズで行われる。バイナリコード生成時には、型パラメータの共変性・反変性は適切な型消去(Type Erasure)またはボックス化の最適化を経て処理されるため、動的なディスパッチコストが増えることはない。
- 避けるべきアンチパターン: 共変・反変を無理に適用しようとして、インターフェースを細分化しすぎると、IDEの補完効率が落ち、コードの可読性が致命的に下がる。「本当にリーダーなのか、シンクなのか」という単一責任の原則(SRP)に従った設計を行っていれば、自然と変性の方向は美しく決まる。
—
チーフアーキテクトからの最終提言
Hackにおける厳格な型システムは、単なる「バグ発見ツール」ではない。それは、「開発者の頭の中にあるドメインの制約を、コードという物理世界に数学的厳密さで固定するための最強の言語機能」だ。
ジェネリクスの変性(`+` と `-`)を恐れるな。これらを使いこなせるようになった時、君の書くコードは「動くだけの脆弱なスクリプト」から「拡張性と堅牢性を極限まで高めたエンタープライズ・アーキテクチャ」へと昇華する。
次のコードレビューでは、チームメンバーのジェネリクス設計をこの視点から厳しく、そして愛を持ってチェックしてやってほしい。