こんにちは!Hack言語の世界へようこそ。
他のプログラミング言語、例えばPHPやTypeScript、Javaなどを触ってきた方なら、「ジェネリクス(総称型)」という強力な仕組みはお馴染みですよね。
でも、ジェネリクスを使いこなそうとしたときに、ふとこんな壁にぶつかったことはありませんか?
「あれ? `Vector
そう、ここで登場するのが今回のお題である「Variance(変性:共変・反変)」です。
この概念は、静的型システムを極める上での最大の難所であり、同時に「HHVMの型チェッカーがなぜ最強の盾になり得るのか」を理解するための核心部分でもあります。
今回は、初心者の方でもスッと脳内にイメージできるように、優しく、かつ本質的な部分まで徹底的に紐解いていきましょう!ここをクリアすれば、Hackの型システムはもう怖くありませんよ。
—
1. そもそも「Variance(変性)」ってなに?
ジェネリクス型(`Box
言葉だけだと難しく聞こえますよね。まずは身近な「動物の親子関係」でイメージしてみましょう。
- Animal(動物):親クラス
- Dog(犬):`Animal` を継承した子クラス
ここで、「犬の箱(Box
「犬は動物の一種なのだから、『犬の箱』は『動物の箱』として扱えるはず!」……直感的にはそう思いますよね?
実は、その箱を「読み取る(読むだけ)」のか、それとも「書き込む(入れるだけ)」のかによって、安全性のルールがガラリと変わるのです。これがVarianceの本質です。
HackのStrict Mode(厳格モード)の世界では、この安全性をコンパイル時(型チェック時)に完璧に担保してくれます。
—
2. 共変(Covariance):「〜のサブタイプであれ」
まずは共変(Covariance)から見ていきましょう。
共変とは、「型引数の継承関係の向きが、そのままジェネリクス全体の継承関係の向きと一致する」性質のことです。
Hackでは、インターフェイスやベクターなどの「読み取り専用(Producer)」な構造体で共変(`+` を付与)を指定できます。
イメージ図:共変の流れ
[Dog] ──(子)──> [Animal]
↓ ↓
[Box
実践コード例(Hack Strict Mode)
Hackで共変を定義するには、型パラメータに `+` を付けます。
<
namespace HackGuide;
// 読み取り専用のリーダーインターフェイス
interface IReader<<+T>> {
// Tを「返す(Produceする)」だけのメソッド
public function get(): T;
}
class Animal {}
class Dog extends Animal {}
// Dog専用のリーダー
class DogReader implements IReader
public function get(): Dog {
return new Dog();
}
}
function processAnimals(IReader
// Animalが取れることが保証されているので安全
$animal = $reader->get();
}
function main(): void {
$dogReader = new DogReader();
// IReader
// 共変(+T)のおかげで、これが型エラーなく通ります。
processAnimals($dogReader);
}
ここがポイント:
`IReader
—
3. 反変(Contravariance):「逆向きのロジック」
次は、少し頭がこんがらがしやすい反変(Contravariance)です。
反変とは、「型引数の継承関係の向きが、ジェネリクス全体では『逆向き』になる」という不思議な性質です。
Hackでは、データの「受け手(Consumer)」になる場合に反変(`-` を付与)を使います。
イメージ図:反変の流れ
[Dog] ──(子)──> [Animal]
↑ ↑
[Box
実践コード例(Hack Strict Mode)
反変を定義するには、型パラメータに `-` を付けます。
<
namespace HackGuide;
// 書き込み・消費専用のコンシューマインターフェイス
interface IConsumer<<-T>> {
// Tを「受け取る(Consumeする)」だけのメソッド
public function consume(T $item): void;
}
class Animal {}
class Dog extends Animal {}
// Animalを処理できるコンシューマ
class AnimalConsumer implements IConsumer
public function consume(Animal $item): void {
// 何らかの動物処理
}
}
function runDogTraining(IConsumer
// ここではDogを受け取れるコンシューマが必要
$consumer->consume(new Dog());
}
function main(): void {
$animalConsumer = new AnimalConsumer();
// IConsumer
// 逆に思えますが、反変(-T)のおかげでこれが安全に成立します。
runDogTraining($animalConsumer);
}
なぜこれが安全なの?(脳内トレースしてみよう)
`runDogTraining` は「Dogを受け取って処理してくれる人」を探しています。
そこに渡された `$animalConsumer` は「どんなAnimalでも処理できる人」です。
「どんなAnimalでも処理できる人」に「Dog」を渡すのは、当然めちゃくちゃ安全ですよね? だから、「Animalのコンシューマ」は「Dogのコンシューマ」の代わりが務まるのです。これが反変のカラクリです。
—
4. 陥りやすい文法エラーとHHVMの優しさ
HackのStrict Modeで開発していると、次のようなエラーに直面することがあります。
> Type error: Invalid argument (ontravariant position)
> または
> Type error: Non-variant generic parameter used where covariance was expected
初心者がやりがちなミスは、「読み書き両方を行う(Invariant / 非変な)クラスのつもりで、勝手に共変や反変を適用しようとすること」です。
// ❌ やってはいけない例:読み書き両方するのに共変をつけようとする
class Box<<+T>> {
private T $item;
public function set(T $item): void { // エラー!
$this->item = $item;
}
public function get(): T {
return $this->item;
}
}
なぜエラーになるのか?
もしこれが許されると、次のようなバグ(型安全性崩壊)が起き得ます。
1. `Box
2. それを共変のルールで `Box
3. その `Box
4. 元の `Box
HHVMの型チェッカーは、こうした悲劇が実行時(Runtime)に起きるのを、コンパイル時に100%阻止してくれます。だからこそ、「書き込み(メソッドの引数)に使われる型は反変の位置」「読み取り(メソッドの戻り値)に使われる型は共変の位置」という厳格なルールがあるのです。
—
まとめ:ここをクリアすればHackの基本はバッチリマスター!
いかがでしたでしょうか?
Variance(共変・反変)の概念は、最初は少し抽象的で難しく感じるかもしれませんが、整理すると非常にシンプルです。
- 共変(`+T`):データを「取り出す(Producer)」だけなら、子から親への代入が安全にできる!
- 反変(`-T`):データを「受け入れる(Consumer)」だけなら、親から子への代入が安全にできる!
- 非変(指定なし):読み書き両方やるなら、型は完全に一致させなきゃダメ!
HHVMの強力な静的型チェッカーは、私たちがうっかり犯してしまう型ミスの可能性を、開発の段階で優しく、かつ厳しく見つけてくれます。このルールを味方につければ、大規模なコードベースでもビクともしない、極めて堅牢なHackアプリケーションを構築できるようになりますよ。
それでは、次回のHack極限知見でお会いしましょう。Happy Coding!