境界を支配せよ:Hackにおける変位(Variance)と型安全性の深淵
Hackの型システムを「PHPの単なる補強」と捉えているなら、君の設計は未完成だ。我々が構築したこの言語は、単に実行時のエラーを減らすためのものではない。コンパイル時のチェックのみで「論理的にあり得ない状態」を排除するための、数学的かつ強固なフレームワークだ。
今回は、多くのエンジニアが直感的に「なんとなく」で書きがちなジェネリクスにおける共変(Covariance)と反変(Contravariance)、すなわち「変位(Variance)」の正体を暴く。これこそが、堅牢なAPI設計と疎結合なコンポーネント設計を分かつ分水嶺だ。
—
1. なぜ「型安全」はジェネリクスで崩壊するのか
以下のコードを見てほしい。一見すると型安全に見えるが、これがなぜ危険なのかを論理的に説明できるか?
abstract class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
// これはコンパイルエラーになる
function processAnimals(Vector
$animals[] = new Cat();
}
// 呼び出し側
$dogs = Vector {new Dog()};
processAnimals($dogs); // もしこれが通れば、DogのVectorにCatが混入する!
ジェネリクスが「不変(Invariant)」である理由、それは「読み込み」と「書き込み」の双方向の権限を同時に持つからだ。`Vector
—
2. 共変(Covariance):読み取り専用の特権
「あるクラスのサブタイプは、そのクラスのジェネリクスを継承できる」というルールだ。Hackでは `<<__Covariant>>` アノテーションを用いてこれを明示できる。
最も一般的なユースケースは「読み取り専用のコレクション」だ。
interface ReadableCollection<+T> {
public function get(int $index): T;
}
class AnimalStore implements ReadableCollection
public function get(int $index): Animal { / … / }
}
// 以下の代入は安全である
ReadableCollection
`+T` は「この型はTを返すことしかしない(Producerである)」とコンパイラに誓約している。そのため、`Dog`を期待する場所に`Animal`が混入するリスクをコンパイラが完全にシャットアウトする。
—
3. 反変(Contravariance):書き込み専用の謙虚さ
逆に、消費することに特化した型は「反変」になる。これはコールバック関数やイベントハンドラにおいて最強の力を発揮する。
interface Consumer<-T> {
public function consume(T $item): void;
}
// Animalを処理できるものは、当然Dogも処理できる
function processDog(Consumer
$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
}
// 実装例:汎用的なロガー
class GenericLogger implements EventHandler
public function handle(EventView
// Animal以下の型なら全てログ出力可能
echo “Processing: ” . get_class($event->getData());
}
}
この設計の美しさは、`GenericLogger` が `Dog` 専用のハンドラとして注入されても、型チェッカーが一切の文句を言わず、かつ実行時に予期せぬキャストエラーが発生しない点にある。
—
結論:コードの重みを感じろ
ジェネリクスにおける `+` と `-` は、単なる装飾ではない。それは「この型はデータの所有者なのか、それとも通過点なのか」という設計思想の表明だ。
- 共変 (`+T`): データを供給する(Produce)。サブタイプへの代入が安全。
- 反変 (`-T`): データを消費する(Consume)。スーパータイプへの代入が安全。
この法則を理解した君なら、もう `mixed` 型で型安全を汚す必要はないはずだ。Hackの型システムは、君が正しく指示を出せば、どんなに巨大なコードベースであっても鉄壁の守りを見せる。
さあ、コードを書け。型チェッカーが「合格」と言ったとき、そこにバグが潜む余地はない。それがHHVMのアーキテクチャが生み出す、圧倒的な安定感の正体だ。