【実務・中級編】【上級者向け】Hackにおける共変(Covariance)と反変(Contravariance):ジェネリクス型における代入ルールの正体 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

境界を支配せよ:Hackにおける変位(Variance)と型安全性の深淵

Hackの型システムを「PHPの単なる補強」と捉えているなら、君の設計は未完成だ。我々が構築したこの言語は、単に実行時のエラーを減らすためのものではない。コンパイル時のチェックのみで「論理的にあり得ない状態」を排除するための、数学的かつ強固なフレームワークだ。

今回は、多くのエンジニアが直感的に「なんとなく」で書きがちなジェネリクスにおける共変(Covariance)と反変(Contravariance)、すなわち「変位(Variance)」の正体を暴く。これこそが、堅牢なAPI設計と疎結合なコンポーネント設計を分かつ分水嶺だ。

—

1. なぜ「型安全」はジェネリクスで崩壊するのか

以下のコードを見てほしい。一見すると型安全に見えるが、これがなぜ危険なのかを論理的に説明できるか?

abstract class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

// これはコンパイルエラーになる
function processAnimals(Vector $animals): void {
$animals[] = new Cat();
}

// 呼び出し側
$dogs = Vector {new Dog()};
processAnimals($dogs); // もしこれが通れば、DogのVectorにCatが混入する!

ジェネリクスが「不変(Invariant)」である理由、それは「読み込み」と「書き込み」の双方向の権限を同時に持つからだ。`Vector`はTを生成(Producer)することも消費(Consumer)することもできる。だからこそ、代入先と代入元の型は完全に一致しなければならない。これを緩和するのが「変位」の役割だ。

—

2. 共変(Covariance):読み取り専用の特権

「あるクラスのサブタイプは、そのクラスのジェネリクスを継承できる」というルールだ。Hackでは `<<__Covariant>>` アノテーションを用いてこれを明示できる。

最も一般的なユースケースは「読み取り専用のコレクション」だ。

interface ReadableCollection<+T> {
public function get(int $index): T;
}

class AnimalStore implements ReadableCollection {
public function get(int $index): Animal { / … / }
}

// 以下の代入は安全である
ReadableCollection $dogs = new AnimalStore();

`+T` は「この型はTを返すことしかしない(Producerである)」とコンパイラに誓約している。そのため、`Dog`を期待する場所に`Animal`が混入するリスクをコンパイラが完全にシャットアウトする。

—

3. 反変(Contravariance):書き込み専用の謙虚さ

逆に、消費することに特化した型は「反変」になる。これはコールバック関数やイベントハンドラにおいて最強の力を発揮する。

interface Consumer<-T> {
public function consume(T $item): void;
}

// Animalを処理できるものは、当然Dogも処理できる
function processDog(Consumer $consumer): void {
$consumer->consume(new Dog());
}

class AnimalLogger implements Consumer {
public function consume(Animal $a): void { / 全てのAnimalをログに出す / }
}

// これは安全!
processDog(new AnimalLogger());

`Consumer<-T>` は、より広範な型(Animal)を受け入れることができる設計ならば、特定の型(Dog)に対しても安全に作用できる。これが反変の論理だ。

—

4. プロダクションコードにおける実践的アプローチ

実務で私が設計する際、ジェネリクスを扱うインターフェースには必ず「変位」を明示する。これにより、コンポーネント間の結合度を下げつつ、型安全性を高次元で維持できる。

設計パターン:堅牢なイベントリスナー

// 読み取り専用のイベントデータ
interface EventView<+T> {
public function getData(): T;
}

// 特定の型を処理するハンドラ(反変)
interface EventHandler<-T> {
public function handle(EventView $event): void;
}

// 実装例:汎用的なロガー
class GenericLogger implements EventHandler {
public function handle(EventView $event): void {
// Animal以下の型なら全てログ出力可能
echo “Processing: ” . get_class($event->getData());
}
}

この設計の美しさは、`GenericLogger` が `Dog` 専用のハンドラとして注入されても、型チェッカーが一切の文句を言わず、かつ実行時に予期せぬキャストエラーが発生しない点にある。

—

結論:コードの重みを感じろ

ジェネリクスにおける `+` と `-` は、単なる装飾ではない。それは「この型はデータの所有者なのか、それとも通過点なのか」という設計思想の表明だ。

  • 共変 (`+T`): データを供給する(Produce)。サブタイプへの代入が安全。
  • 反変 (`-T`): データを消費する(Consume)。スーパータイプへの代入が安全。

この法則を理解した君なら、もう `mixed` 型で型安全を汚す必要はないはずだ。Hackの型システムは、君が正しく指示を出せば、どんなに巨大なコードベースであっても鉄壁の守りを見せる。

さあ、コードを書け。型チェッカーが「合格」と言ったとき、そこにバグが潜む余地はない。それがHHVMのアーキテクチャが生み出す、圧倒的な安定感の正体だ。

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