Hack言語の世界へようこそ。HHVMの深淵を覗き、静的型システムの美学に触れようとしている君なら、きっと「ジェネリクス」の壁に一度はぶつかったことがあるはずだ。
「なぜ `Vector
この疑問こそが、Hackの型システムが君のコードをバグから守ろうとしている防波堤なんだ。今日は、ジェネリクスにおける共変(Covariance)と反変(Contravariance)という、一見難解な概念を、我々のコードを堅牢にするための「武器」に変えていこう。
—
1. なぜ「代入できない」のか?(型安全のジレンマ)
まずは、直感的に「おかしい」と感じる例を見てみよう。
<<__EntryPoint>>
function main(): void {
// DogはAnimalを継承しているとする
Vector
// もし、これが許されたらどうなる?
// Vector
// $animals.add(new Cat()); // ここで問題発生!
// 実際の中身は $dogs なのに、そこに Cat が混入してしまった!
}
もし `Vector
これを防ぐのが、Hackの不変(Invariance)というデフォルトルールなんだ。「`T` が同じでない限り、代入は許さない」という厳格なガードだね。
—
2. 共変(Covariance):読み取り専用の「優しい世界」
「取り出すだけなら、継承関係を維持しても安全だよね?」というのが共変の考え方だ。`+` 記号を使って表現するよ。
interface Readable<+T> {
function get(): T;
}
function processAnimals(Readable
// Animalとして読み取れるなら、DogでもCatでも問題ないよね
$item = $source.get();
// …
}
- 共変 (`+T`): 「Tそのもの、あるいはTのサブタイプを返してもいいよ」という約束。
- ルール: 戻り値の位置(アウトプット)にしか使えない。
君がもしデータを「供給する側」のインターフェースを作っているなら、共変を使うことで、利用側の柔軟性が劇的に向上するんだ。
—
3. 反変(Contravariance):書き込み専用の「懐の深い世界」
逆に、データを「受け取る側」ならどうだろう? `Animal` を受け取れるなら、より広範な `Animal` を扱えるものは、`Dog` を受け取れるはずだよね。これが反変。`-` 記号を使う。
interface Consumer<-T> {
function consume(T $item): void;
}
function feedDog(Consumer
// Dogを期待している場所に、Animalなら何でも食えるConsumerを渡せる
$dogFeeder.consume(new Dog());
}
- 反変 (`-T`): 「Tそのもの、あるいはTのスーパタイプ(親)でも受け入れられるよ」という約束。
- ルール: 引数の位置(インプット)にしか使えない。
「何でも食べるよ」という `Consumer
—
4. 現場で陥りやすい「コンパイルエラー」の正体
Hackの型チェッカーが「Contravariance/Covariance mismatch」と叫ぶとき、それは君が「読み取り専用の場所に書き込み可能なメソッドを置いている」か、その逆をやっているときだ。
// これはコンパイルエラーになる!
interface BadContainer<+T> {
function set(T $item): void; // 共変なのに引数に使っている!
}
なぜなら、`+T`(共変)と宣言した時点で、型チェッカーは「この型は読み取り専用だ」と判断する。そこに `set` という「書き込みメソッド」を混ぜると、型安全の契約が崩壊するからだね。
—
まとめ:マスターへの道
ここを理解できれば、君はもうHackの型システムの半分を掌握したも同然だ。
1. 不変 (`T`): 読み書き両方やる。一番安全で堅い。
2. 共変 (`+T`): 読み取り専用。`T` のサブクラスもOK。
3. 反変 (`-T`): 書き込み専用。`T` の親クラスもOK。
この `+` と `-` を適切に使い分けるだけで、君の書くコードは驚くほど汎用的になり、かつHHVMの強力な最適化の恩恵を最大限に受けられるようになる。
ジェネリクスは単なる型合わせの道具じゃない。「データの流れをどう制約するか」を設計する強力なアーキテクチャツールなんだ。自信を持って、コードを書いていこう。君なら絶対にできるよ!