こんにちは!HHVMの内部構造やHackの厳格な型システムの底知れぬ魅力に憑りつかれたあなたなら、日々の開発で「おっ、ここで型エラーか……」とニヤリとすることも多いのではないでしょうか。
今回は、Hackの静的型チェッカー(Typechecker)を完全手玉に取るための最難関トピック、「ジェネリクスにおける共変(Covariance)と反変(Contravariance)」の世界へあなたを誘います。
他の言語(JavaのワイルドカードやC#のout/inキーワードなど)で挫折した人でも大丈夫。ここをクリアすれば、Hackの型システムの本質が手に取るように分かり、複雑なアーキテクチャでも迷わず安全なコードが書けるようになりますよ。
—
1. なぜジェネリクスの代入で頭が痛くなるのか?
まずは、次のコードを見てください。あなたはスーパークラス `Animal` と、それを継承したサブクラス `Dog` を持っています。
<<__Strict>>
namespace HackMaster;
class Animal {}
class Dog extends Animal {}
function processAnimals(Vector
// 何らかの処理
}
ここで、`Vector
> “Expected `Vector
「あれ? `Dog` は `Animal` の一種なんだから、`Vector
実はここに、ジェネリクスと継承が交わるときに発生する「型の罠」が潜んでいます。
—
2. そもそも「変性(Variance)」ってなに?
変性(Variance)とは、「ある型同士の継承関係(親子関係)が、それらを包むジェネリック型(例:`Container
Hack(および多くの静的型付け言語)では、デフォルトのジェネリクスは「非変(Invariant)」です。つまり、`Dog extends Animal` であっても、`Container
なぜそんなに冷酷なルールになっているのでしょうか? その理由を、次のコードでシミュレーションしてみましょう。
// 【架空の危険な世界線】もし Vector
<<__Strict>>
namespace HackMaster;
function addCatToAnimals(Vector
// Animal の仲間なら、猫(Cat)を追加しても安全なはず!
$animals[] = new Cat();
}
class Cat extends Animal {}
もし `Vector
だからこそ、デフォルトではジェネリクスは非変(厳密に一致しなければならない)なのです。
—
3. 救世主「共変(Covariance: `+`)」:読むだけの世界
「でも、データを読み取るだけなら、`Dog` のコンテナを `Animal` のコンテナとして扱っても安全なはずだよね?」
その通りです。その「読み取り専用(Producer)」の文脈において、親子関係の向きをそのまま維持することを共変(Covariance)と呼びます。
Hackでは、ジェネリックパラメータの前に `+` をつけることで、その型が「共変」であることを型チェッカーに教えられます。
<<__Strict>>
namespace HackMaster;
class Animal {}
class Dog extends Animal {}
// 共変インターフェース(データの「生産者」)
interface IProducer<<__Covariant T>> {
public function get(): T;
}
function printAnimalName(IProducer
// ここで Animal として扱える
}
<<__EntryPoint>>
function main(): void {
// IProducer
// $dogProducer = …;
// printAnimalName($dogProducer);
}
- イメージ: `Dog` は `Animal` なので、`IProducer
` は `IProducer ` の「子」とみなせる(方向が一致=共変)。 - 制限: 共変に指定した型パラメータ(`T`)は、メソッドの返り値の型としてのみ使用できます。引数に使うことは許されません(前述の「猫の混入」を防ぐためです)。
—
4. もう一つの顔「反変(Contravariance: `-`)」:受け入れる世界
共変があればその逆もあります。それが反変(Contravariance)です。
こちらは、データの書き込み・消費(Consumer)を行う場合に真価を発揮します。
Hackでは、ジェネリックパラメータの前に `-` をつけて表現します。
<<__Strict>>
namespace HackMaster;
// 反変インターフェース(データの「消費者」)
interface IConsumer<<__Contravariant T>> {
public function consume(T $value): void;
}
function handleDogConsumer(IConsumer
// …
}
<<__EntryPoint>>
function testContravariance(): void {
// 驚くべきことに、IConsumer
// なぜなら、「Animalを受け取れる仕組み」があれば、「Dogを受け取れ」と言われても余裕で対処できるから。
// $animalConsumer = new AnimalConsumer();
// handleDogConsumer($animalConsumer);
}
- イメージ: `Dog extends Animal` なのに、コンテナの代入関係は逆転して `IConsumer
` が `IConsumer ` の代わりになる(方向が逆=反変)。 - 制限: 反変に指定した型パラメータ(`T`)は、メソッドの引数の型としてのみ使用できます。
—
5. まとめ:ここをクリアすればHackの型マスター!
変性のルールを、HHVMの厳格な世界観に合わせて一枚の表にまとめました。
| 変性の種類 | 記号 | 意味・方向 | メソッド内での使用許可 | ユースケースのイメージ |
| :— | :— | :— | :— | :— |
| 非変 (Invariant) | なし | `A` と `B` に親子関係なし | 引数・返り値どちらもOK | 読み書き両方行うコレクション (`Vector`, `Map`) |
| 共変 (Covariant) | `+` (`<<__Covariant>>`) | `A` が子なら `C` も子 (`A` = `C`) | 返り値のみ | データの生産者・イテレータ (`Iterator<+T>`) |
| 反変 (Contravariant) | `-` (`<<__Contravariant>>`) | `A` が子なら `C` は親 (`A` ≠ `C`) | 引数のみ | コールバック・コンシューマ (`Consumer<-T>`) |
複雑なフレームワークの設計や、独自のドメインモデルでジェネリクスを巧みに使いこなすとき、この共変・反変の知識があなたを強力にサポートしてくれます。「型チェッカーに怒られない美しい設計」がスラスラ書けるようになると、Hackコーディングが何倍 aussi 楽しくなりますよ!
ここをクリアしたあなたなら、もうHackの基本で躓くことはありません。自信を持って、より高みを目指していきましょう!