【入門編】Hackにおける『Variance(共変・反変)』の理解:ジェネリクスを安全に扱うための理論と実践 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

こんにちは!Hack言語の世界へようこそ。
他のプログラミング言語、例えばPHPやTypeScript、Javaなどを触ってきた方なら、「ジェネリクス(総称型)」という強力な仕組みはお馴染みですよね。

でも、ジェネリクスを使いこなそうとしたときに、ふとこんな壁にぶつかったことはありませんか?
「あれ? `Vector` を期待されているところに `Vector` を渡したら、型チェッカーに怒られてしまった……。これって継承関係にあるのにおかしいのでは?」

そう、ここで登場するのが今回のお題である「Variance(変性:共変・反変)」です。
この概念は、静的型システムを極める上での最大の難所であり、同時に「HHVMの型チェッカーがなぜ最強の盾になり得るのか」を理解するための核心部分でもあります。

今回は、初心者の方でもスッと脳内にイメージできるように、優しく、かつ本質的な部分まで徹底的に紐解いていきましょう!ここをクリアすれば、Hackの型システムはもう怖くありませんよ。

—

1. そもそも「Variance(変性)」ってなに?

ジェネリクス型(`Box` など)と、その型引数(`T`)の関係性が、継承関係(子から親へ、あるいはその逆)のときにどう振る舞うかを示すルールのことです。

言葉だけだと難しく聞こえますよね。まずは身近な「動物の親子関係」でイメージしてみましょう。

  • Animal(動物):親クラス
  • Dog(犬):`Animal` を継承した子クラス

ここで、「犬の箱(Box)」 と 「動物の箱(Box)」 があったとします。
「犬は動物の一種なのだから、『犬の箱』は『動物の箱』として扱えるはず!」……直感的にはそう思いますよね?

実は、その箱を「読み取る(読むだけ)」のか、それとも「書き込む(入れるだけ)」のかによって、安全性のルールがガラリと変わるのです。これがVarianceの本質です。

HackのStrict Mode(厳格モード)の世界では、この安全性をコンパイル時(型チェック時)に完璧に担保してくれます。

—

2. 共変(Covariance):「〜のサブタイプであれ」

まずは共変(Covariance)から見ていきましょう。
共変とは、「型引数の継承関係の向きが、そのままジェネリクス全体の継承関係の向きと一致する」性質のことです。

Hackでは、インターフェイスやベクターなどの「読み取り専用(Producer)」な構造体で共変(`+` を付与)を指定できます。

イメージ図:共変の流れ

[Dog] ──(子)──> [Animal]
↓ ↓
[Box] ──(同じ向き)──> [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 $reader): void {
// Animalが取れることが保証されているので安全
$animal = $reader->get();
}

function main(): void {
$dogReader = new DogReader();

// IReader を IReader に渡している!
// 共変(+T)のおかげで、これが型エラーなく通ります。
processAnimals($dogReader);
}

ここがポイント:
`IReader` は `IReader` のサブタイプ(部分型)として扱われます。データを取り出す(Producer)だけなら、犬を取り出そうが動物を取り出そうが危険は一切ありませんよね。だから共変は「安全」なのです。

—

3. 反変(Contravariance):「逆向きのロジック」

次は、少し頭がこんがらがしやすい反変(Contravariance)です。
反変とは、「型引数の継承関係の向きが、ジェネリクス全体では『逆向き』になる」という不思議な性質です。

Hackでは、データの「受け手(Consumer)」になる場合に反変(`-` を付与)を使います。

イメージ図:反変の流れ

[Dog] ──(子)──> [Animal]
↑ ↑
[Box] <──(逆向き)── [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 $consumer): void {
// ここではDogを受け取れるコンシューマが必要
$consumer->consume(new Dog());
}

function main(): void {
$animalConsumer = new AnimalConsumer();

// IConsumer を 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` に対して、まったく関係ない `Cat(猫)` を `set()` してしまいます。
4. 元の `Box` の中身を取り出そうとしたら、中身がCatなのにDogとして扱われてしまい、実行時クラッシュ(またはメモリ破壊)が起きます。

HHVMの型チェッカーは、こうした悲劇が実行時(Runtime)に起きるのを、コンパイル時に100%阻止してくれます。だからこそ、「書き込み(メソッドの引数)に使われる型は反変の位置」「読み取り(メソッドの戻り値)に使われる型は共変の位置」という厳格なルールがあるのです。

—

まとめ:ここをクリアすればHackの基本はバッチリマスター!

いかがでしたでしょうか?
Variance(共変・反変)の概念は、最初は少し抽象的で難しく感じるかもしれませんが、整理すると非常にシンプルです。

  • 共変(`+T`):データを「取り出す(Producer)」だけなら、子から親への代入が安全にできる!
  • 反変(`-T`):データを「受け入れる(Consumer)」だけなら、親から子への代入が安全にできる!
  • 非変(指定なし):読み書き両方やるなら、型は完全に一致させなきゃダメ!

HHVMの強力な静的型チェッカーは、私たちがうっかり犯してしまう型ミスの可能性を、開発の段階で優しく、かつ厳しく見つけてくれます。このルールを味方につければ、大規模なコードベースでもビクともしない、極めて堅牢なHackアプリケーションを構築できるようになりますよ。

それでは、次回のHack極限知見でお会いしましょう。Happy Coding!

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